다형성
다형성(polymorphism)는 상속과 함께 객체지향에서 중요한 요소 중 하나입니다. 다형성이란 여러 형태를 가질 수 있는 특성을 의미하며 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 구현한 기능입니다.
다음 예제를 통해 다형성에 대해 확인해보겠습니다.
class Tv { boolean power; int channel; void power() { power = !power; } void channelUp() { ++channel; } void channelDown() { --channel; } } class CaptionTv extends Tv { String text; void caption() {}; }
위의 코드를 기준으로 다음과 같이 인스턴스를 생성하여 참조할 수 있습니다.
Tv tv = new CaptionTv();
CaptionTv captionTv = new CaptionTv();
이렇게 인스턴스를 생성하여 참조할 경우, 자식 클래스의 참조변수 captionTv는 모든 멤버를 사용할 수 있지만 부모 클래스 참조변수 tv는 자식 클래스의 멤버 사용이 제한됩니다. 정리하면 같은 인스턴스에 대해 참조변수의 타입이 달라지면 사용할 수 있는 멤버의 개수도 달라지게 됩니다.
하지만 위와 같이 인스턴스를 생성하여 참조할 경우 CaptionTv captionTv = new CaptionTv(); 부분에서 컴파일 에러가 발생합니다. 에러가 발생하는 이유는 자식 클래스의 참조변수로 부모 클래스의 인스턴스를 참조했는데 이 때 자식 클래스에만 있는 멤버를 사용할 경우 문제가 발생하기 때문입니다.
이러한 다형성의 특징을 정리하면 다음과 같습니다.
- 부모 클래스의 참조변수로 자식 클래스의 인스턴스 참조 가능.
- 자식 클래스의 참조변수로 부모 클래스의 인스턴스 참조 불가능.
※ 자바의 클래스는 상속을 통해 확장될 수는 있어도 축소될 수는 없음. 따라서 부모 클래스의 멤버는 자식 클래스보다 항상 같거나 적음.
1.1 참조변수의 형변환
기본형 변수와 마찬가지로 참조변수도 형변환이 가능합니다. 하지만 이 경우는 상속관계의 클래스 사이에서만 가능하다는 전제 조건이 있습니다.
참조변수의 형변환은 다음과 같이 이루어집니다.
자식타입 참조변수 → 부모타입 참조변수 : 업캐스팅(Up-casting) 이라고하며 형변환 생략 가능
부모타입 참조변수 → 자식타입 참조변수 : 다운캐스팅(Down-casting) 이라고하며 형변환 생략 불가능 (캐스팅 연산자 사용 필요)
다음 예제를 통해 참조변수의 형변환에 대해 확인해보겠습니다.
class Car { String color; int door; void drive() { System.out.println("drive~~~"); } void stop() { System.out.println("stop!"); } } class FireEngine extends Car { void water() { System.out.println("water!!!"); } } public class JavaApp { public static void main(String[] args) { Car car = null; Car car2 = null; FireEngine fe = new FireEngine(); FireEngine fe2 = null; fe.water(); car = fe; // car = (Car) fe; 와 같으며, 캐스트 연산자 없이 형변환이 생략된 형태. fe2 = (FireEngine) car; // 부모타입 -> 자식타입 형변환 fe2.water(); car2 = new Car(); fe = null; car2.drive(); fe = (FireEngine) car2; // 자식타입 -> 부모타입 형변환 (컴파일은 되지만 실행시 에러 발생) } }
위의 예제를 실행하여 확인해보면, main() 메서드의 30번째 줄에서 부모 타입 참조변수가 자식 타입의 참조변수로 형변환을 해주었기 때문에 32번째 줄이 이상없이 실행되는 것을 확인할 수 있습니다.
그러나 이후의 39번째 줄을 보면 컴파일은 되지만 실행시 에러가 발생하는 것을 확인할 수 있는데, 자식 타입의 참조변수로 부모 타입의 인스턴스를 참조했기 때문에 에러가 발생한 것을 확인할 수 있습니다.
1.2 instanceof 연산자
앞서 확인해본 형변환의 경우처럼 참조변수가 가리키는 인스턴스의 타입을 아는 것이 중요하다는 것을 확인했습니다. instanceof 연산자는 참조변수가 참조하는 인스턴스의 실제 타입을 알기 위해 사용합니다.
instanceof 연산자는 true/false 결과값을 반환하며, true의 경우 해당 인스턴스의 타입으로 형변환이 가능하다는 것을 의미합니다.
다음 예제를 통해 instanceof 연산자에 대해 확인해보겠습니다.
class Car {} class FireEngine extends Car {} class JavaApp { public static void main(String[] args) { FireEngine fe = new FireEngine(); if(fe instanceof FireEngine) { System.out.println("FireEngine instance"); } if(fe instanceof Car) { System.out.println("Car instance"); } if(fe instanceof Object) { System.out.println("Object instance"); } } }
실행결과는 다음과 같습니다.
FireEngine instance
Car instance
Object instance
FireEngine 클래스가 Car 클래스를 상속 받았고 Car 클래스는 Object 클래스를 상속 받았기 때문에, FireEngine 클래스의 인스턴스는 Car 인스턴스와 Object 인스턴스를 포함하고 있는 것을 확인할 수 있습니다.
1.3 참조변수와 인스턴스의 연결
상속관계에 있는 부모/자식 클래스에서 멤버의 호출은 다음과 같은 특징을 같습니다.
메서드 : 자식 클래스에서 오버라이딩했을 경우 타입에 상관없이 해당 인스턴스의 메서드가 호출됨.
멤버변수 : 참조변수의 타입에 따라 호출되는 멤버변수가 달라짐. (부모/자식 클래스의 멤버변수명이 동일한 경우에만 해당)
다음 예제를 통해 참조변수의 타입에 따른 멤버 호출의 차이점을 확인해보겠습니다.
class Parent { int x = 100; void method() { System.out.println("Parent Method"); } } class Child extends Parent { int x = 200; // 부모 클래스의 멤버변수와 중복 정의. void method() { System.out.println("Child Method"); } } class JavaApp { public static void main(String[] args) { Parent parent = new Child(); Child child = new Child(); System.out.println("parent.x = " + parent.x); System.out.println("child.x = " + child.x); parent.method(); child.method(); } }
실행하면 다음과 같은 결과를 확인할 수 있습니다.
parent.x = 100
child.x = 200
Child Method
Child Method
메서드의 경우 참조변수의 타입에 상관없이 참조하는 인스턴스에 포함된 메서드가 호출되지만, 중복 정의된 멤버변수의 경우 참조하는 인스턴스가 같아도 참조변수의 타입에 따라 멤버변수를 다르게 호출하는 것을 확인할 수 있습니다.
1.4 매개변수의 다형성
메서드의 매개변수에도 다형성의 특징이 적용됩니다.
다음 예제를 통해 매개변수의 다형성에 대해 확인해보겠습니다.
class Product { int price = 0; Product(int price) { this.price = price; } } class Tv extends Product { Tv() { super(100); } } class Computer extends Product { Computer() { super(200); } } class Microwave extends Product { Microwave() { super(300); } } class Seller { // 다형성을 적용하여 상속받은 자식 타입의 참조변수를 매개변수로 받음. void setPrice(Product product) { System.out.println("product.price = " + product.price); } } class JavaApp { public static void main(String[] args) { Seller seller = new Seller(); seller.setPrice(new Tv()); seller.setPrice(new Computer()); seller.setPrice(new Microwave()); } }
실행해보면 다음의 결과를 확인할 수 있습니다.
product.price = 100
product.price = 200
product.price = 300
다형성이 적용되어 메서드의 매개변수가 부모 타입인 경우, 상속받은 자식 타입의 참조변수도 받을 수 있다는 것을 확인할 수 있습니다.
1.5 여러 종류의 객체를 배열로 다루기
부모 타입의 참조변수 배열을 사용하여 같은 부모를 가진 서로 다른 객체를 묶어서 배열로 다룰 수 있습니다.
다음 예제를 통해 확인해보겠습니다.
class Product {} class Tv extends Product {} class Computer extends Product {} class Microwave extends Product {} class JavaApp { public static void main(String[] args) { Product[] productArr = new Product[3]; productArr[0] = new Tv(); productArr[1] = new Computer(); productArr[2] = new Microwave(); for(Product product : productArr) { System.out.println(product.getClass().getName()); } } }
실행 결과는 다음과 같습니다.
Tv
Computer
Microwave
이를 통해 공통된 부모 타입의 참조변수 배열을 통해 상속받은 자식 인스턴스를 묶어서 배열로 사용할 수 있는 것을 확인할 수 있습니다.
이상으로 자바의 다형성에 대해서 알아봤습니다.
※ 참고 문헌
남궁성, 『Java의 정석 3rd Edition』, 도우출판(2016), p354 ~ p374. Chapter 07 객체지향 프로그래밍 II