다형성

by 조쉬 posted Sep 13, 2016
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄

다형성 


1. 다형성이란?

다형성은 상속과 깊은 관계가 있다.

객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록함으로써 다형성을 프로그램적으로 구현하였다.


인터페이스와 상속은 둘 다 다형성이라는 객체지향 프로그래밍의 특징을 구현하는 방식이다.

1 다형성: 하나의 객체를 여러 개의 타입으로, 하나의 타입으로 여러 종류의 객체를 여러 가지 모습으로 해석될 수 있는 성격이라고 생각하면 된다.

vo.) 다형성(Ploymorphism): 'poly'는 다양한, 많은/‘morp'는 형태


2 다형성은 하나의 객체를 여러 가지 타입으로 선언할 수 있다는 뜻이다.

다형성은 개발자들에세는 간단히 말해서 하나의 사물(객체)을 다양한 타입으로 선언하고 사용할 수 있다는 의미로 해석해주면 된다. 일반적으로 어떤 객체가 하나의 분류에만 포함되는 것은 아니다. 대한민국의 국민인 동시에, 남자인 동시에, 서울에 사는 사람 등과 같이 이처럼 다형성은 어떤 사물을 여러 가지 시선으로 바라보는 모습을 생각하면 쉽게 이해할 수 있다.


3 Java에서 다형성은 상속과 인터페이스를 통해 이루어진다.

다형성의 의미는 하나의 객체를 다양한 시선(타입)으로 바라볼 수 있게 한다는 의미이다.

중요한 것은 다양한 타입으로 본다는 사실 자체가 아니라 다양한 타입으로 객체를 바라보게 되면 호출할 수 있는 메소드 역시 타입에 따라 달라진다는 것이다. 상속의 오버라이딩을 설명하면서 오버라이딩을 하게 되면 컴파일러는 실제 객체의 메소드를 바라보는 것이 아니라. 변수 선언 타입의 메소드를 본다.

Mouse m = new WheelMouse( );

실제 객체가 WheelMouse이지만 컴파일러는 Mouse 타입의 메소드가 정상적으로 호출되고 있는지에만 관심을 두게 된다.


4 인터페이스가 상속보다 다형성에 더욱 유연함을 제공한다.

인터페이스는 클래스의 선언 뒤에서 여러 개의 인터페이스를 구현할 수 있게 할 수 있다. 이런 이유 때문에 하나의 객체를 여러 개의 타입으로 바라보는 다형성에는 상속보다 인터페이스가 더 큰 유연함을 제공한다고 할 수 있다.

cf.) 인터페이스가 여러 개 올 수 있다는 의미는 다시 말해 ‘여러가지 타입으로 변수를 선언할 수 있다’라는 것이다.(인터페이스를 상속과 결부시키지 말고 다형성의 측면에서 이해해야만 한다. 인터페이스는 다중 구현이라는 말이 더 정확하다.)

인터페이스는 그 목적상 기능을 의미하게 할 수 있다. 즉 어떤 객체가 어떤 기능을 할 수 있는가로 설계할 경우에 기능에 초점을 두고 인터페이스로 설계할 수 있다는 얘기이다. 따라서 이렇게 되면 어떤 객체는 여러 가지 기능을 가지게 된다.

결론적으로 인터페이스를 이용하면 하나의 객체가 여러 개의 기능을 가지는 형태로 보이게 만들어줄 수 있다.

마치 상속에서 부모 타입으로 변수를 선언하고 자식 타입으로 객체를 생성하는 코드와 유사하긴 하지만 인터페이스는 더 다양한 형태로 객체를 정의해줄 수 있다. 이것은 마치 부모 클래스의 기능을 물려받는 모습처럼 선언되기는 하지만 상속보다는 더 많은 종류를 보여줄 수 있게 된다. 이런 모습 때문에 일반적으로 다중 상속의 기능을 활용하기 위해서 인터페이스를 사용한다는 설명되는 경우가 많다.


※ 이슈: 다형성을 반영한 참조변수

1) 메서드를 호출한 실제 객체 타입

2) 멤버변수의 실제 객체 타입


이를 좀 더 구체적으로 말하자면, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.

class Tv{

boolean power;

int channel;

void power(){ 

power = power;

}

void channelUp(){

++channel;

}

void channelDown(){

--channel;

}

}

class CationTv extends Tv{

String text;//캡션을 보여 주기 위한 문자열

void captionTv(){

...

}

}


지금까지 생성된 인스턴스를 다루기 위해서, 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했다. 즉, Tv인스턴스를 다루기 위해서는 Tv타입의 참조변수를 사용하고, CaptionTv인스턴스를 다루기 위해서는 CaptionTv타입의 참조변수를 사용했다.

Tv t =new Tv();

CationTv e = new CaptionTv();

이처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만, Tv와 CaptionTv클래스가 서로 상속관계에 있을 경우, 다음과 같이 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.


Tv t =new  CationTv();


※ 인스턴스를 같은 타입의 참조변수로 참조하는 것과 조상타입의 참조변수로 참조하는 것의 차이점

CationTv c = new CaptionTv();


Tv t =new CaptionTv();

위의 코드에서 CaptionTv 인스턴스 2개를 생성하고, 참조변수 c, t가 생성된 인스턴스를 하나씩 참조하도록 하였다. 이 경우 실제 인스턴스가 CationTv타입이라 할지라도, 참조변수 t로는 CaptionTv인스턴스의 모든 멤버를 사용할 수 없다. Tv 타입의 참조변수로는 CaptionTv 인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다. 따라서, 생성된 CaptionTv 인스턴스의 멤버 중에서 Tv클래스에 정의 되지 않은 멤버, text와 caption()은 참조변수 t로 사용이 불가능하다. 즉, t.text 또는 t.caption()와 같이 할 수 없다는 것이다.

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


cf.) Caption c = new Tv(); // 컴파일 에러

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

--> 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않는다. 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 하는 것이다.

참조변수의 타입이 참조변수가 참고하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 결정한다는 사실을 이해하는 것은 매우 중요하다.


2. 참조변수의 형변환 (기본형변수의 형변환과 구별)


기본형 변수와 같이 참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자손 타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입 참조변수로의 형변환만 가능하다.

(Child-->Person, Person -->Child)

cf.) 바로 위 조상이나 자손이 아닌 간접적인 상속관계, 예를 들면 조상의 조상에 있는 경우에도 형변환이 가능하다. 따라서 모든 참조변수는 모든 클래스의 조상인 Object클래스 타입으로 형변환이 가능하다.


기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이, 참조형 변수의 형변환에서는 자손타입의 참조변수를 조상타입으로 형변환하는 경우에는 형변환을 생략할 수 있다.

(작은 것 --> 큰 것은 형변환 생략가능--> 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않는다. 그래서 형변환을 생략할 수 있도록 한 것이다.

하지만, 큰 것에서 작은 것으로 형변환 할 경우 참조변수가 다룰 수 있는 멤버의 개수를 늘이는 것이므로, 실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 가능성이 있다. 그래서 자손타입으로의 형변환은 생략할 수 없으며, 형변환을 수행하기 전에 instanceof 연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스의 타입을 확인하는 것이 안전하다.


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

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

cf.) Tv t =new Caption(); 도 원래는 Tv t = (Tv)new Caption();의 형태이다.


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!!!");

}

}

실행결과)

water!!!

water!!!


class CastingTest2 {

public static void main(String args[]) {

Car car = new Car();

Car car2 = null;

FireEngine fe = null;

  

car.drive();

fe = (FireEngine)car;// 8번째 줄. 실행 시 에러가 발생한다.(조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것을 허용되지 않는다.--> Child c = new Person();)

fe.drive();

car2 = fe;

car2.drive();

}

}

실행결과)

drive, Brrrr~

java.lang.ClassCastException: Car

at CastingTest2.main(CastingTest2.java:8)

cf.) 캐스트 연산자를 사용하면 서로 상속관계에 있는 클래스 타입의 참조변수간의 형변환은 양방향으로 자유롭게 수행될 수 있다. 그러나 참조변수가 참조하고 있는 인스턴스의 자손타입으로 형변환을 하는 것은 허용되지 않는다.


3. 참조변수와 인스턴스의 연결

조상타입의 참조변수와 자손타입의 참조변수의 차이점이 사용할 수 있는 멤버의 개수에 있다고 배웠다. 
조상클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와 자손타입의 참조변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.
메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
메소드: 실제 인스턴스에 따라
멤버변수: 참조변수의 타입에 따라
cf.) static 메서드는 static변수처럼 참조변수의 타입에 영향을 받는다. 참조변수의 타입에 영향을 받지 않는 것은 인스턴스메서드 뿐이다.

멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손 타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다.
하지만 중복 정의가 되지 않은 경우, 조상타입의 참조변수를 사용했을 때와 자손타입의 참조변수를 사용했을 때의 차이는 없다. 중복된 경우는 참조변수의 타입에 따라 달라지지만, 중복되지 않은 경우 하나뿐이므로 선택의 여지가 없기  때문이다.

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("Parent Method");
}
}

class Child extends Parent {
int x = 200;

void method() {
System.out.println("Child Method");
}
}
실행결과)
p.x=100
Child Method
c.x=200
Child Method
타입은 다르지만, 참조변수 p,c 모두 Child인스턴스를 참조하고 있다. 그리고, Parent클래스와 Child클래스는 서로 같은 멤버들을  정의하고 있다.
이 때 조상타입의 참조변수 p로 Child 인스턴스의 멤버들을 사용하는 것과 자손타입의 참조변수 c로 Child인스턴의 멤버들을 사용하는 것의 차이를 알 수 있다.
메서드인 method()의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child클래스에 정의된 메서드가 호출되지만, 인스턴스변수인 x는 참조변수의 타입에 따라서 달라진다.

class BindingTest2 {
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("Parent Method");
}
}
class Child extends Parent { }
실행결과)
p.x=100
Child Method
c.x=200
Child Method
Child 클래스에는 아무런 멤버도 정의되어 있지 않고 단순히 조상으로부터 멤버들을 상속받는다. 그렇기 때문에 참조변수의 타입에 관계없이 조상의 멤버들을 사용하게 된다. 
이처럼 자손 클래스에서 조상 클래스의 멤버를 중복으로 정의하지 않았을 때는 참조변수의 타입에 따른 변화가 없다.
어느 클래스의 멤버가 호출되어야 할지, 즉 조상의 멤버가 호출되어야할 지, 자손의 멤버가 호출되어야할 지에 대해 선택의 여지가 없기 때문이다. 
참조변수의 타입에 따라 결과가 달라지는 경우는 조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우뿐이다.

class BindingTest3{
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();
System.out.println("c.x = " + c.x);
c.method();
}
}

class Parent {
int x = 100;

void method() {
System.out.println("Parent Method");
}
}

class Child extends Parent {
int x = 200;

void method() {
System.out.println("x=" + x); // this.x와 같다.
System.out.println("super.x=" + super.x);
System.out.println("this.x=" + this.x);
}
}
실행결과)
p.x=100
x=200
super.x=100
this.x=200

c.x=200
x=200
super.x=100
this.x=200
자손클래스 Child에 선언된 인스턴스변수 x와 조상 클래스 Parent로부터 상속받은 인스턴스변수 x를 구분하는데 참조변수 super와 this가 사용된다. 
자손인 Child클래스에서의 super.x는 조상 클래스인 Parent에 선언된 인스턴스변수 x를 뜻하며, this.x 또는 x는 Child클래스의 인스턴스변수 x를 뜻한다. 그래서 위 결과에서 x와 this.x의 값이 같다.
전에 배운 것과 같이 멤버변수들은 주로 private으로 접근을 제한하고, 외부에서는 메서드를 통해서만 멤버변수에 접근할 수 있도록 하지, 이번 예제에서처럼 다른 외부 클래스에서 참조변수를 통해 직접적으로 인스턴스변수에 접근할 수 있게 하지 않는다. 인스턴스변수에 직접 접근하면, 참조변수의 타입에 따라 사용되는 인스턴스변수가 달라질 수 있으므로 주의해야 한다.

4. 매개변수의 다형성
참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다. 아래와 같이 Product, Tv, Computer, Audio, Buyer클래스가 정의되어 있다고 가정하자.
cp.) 메서드 매개변수에 객체와 객체 타입 선언 한다는 것의 의미: 매개변수로 선언된 객체를 사용하겠다는 의미

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;
}
Product 클래스는 Tv와 Computer클래스의 조상이며, Buyer클래스는 제품을 구현하는 사람을 클래스로 표현한 것이다.
Buyer클래스에 물건을 구입하는 기능의 메서드를 추가해보자. 구입할 대상이 필요하므로 매개변수로 구입할 제품을  넘겨받아야 한다. Tv를 살 수 있도록 매개변수를 Tv타입으로 하였다.

void buy(Product p){
money=money-p.price;
bonusPoint=bonusPoint+p.bonusPoint;
}
매개변수가 Product타입의 참조변수라는 것은,  메서드의 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 뜻이다.
--> 앞으로 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면, buy(Product  p)메서드의 매개변수로 받아들여질 수 있다. 따라서, 메서드의 매개변수에 다형성을 적용하면 하나의 메서드로 간단히 처리할 수 있다.

class Product 
{
int price;// 제품의 가격
int bonusPoint;// 제품구매 시 제공하는 보너스점수

Product(int price) {
this.price = price;
bonusPoint =(int)(price/10.0);// 보너스점수는 제품가격의 10%
}
}

class Tv extends Product {
Tv() {
// 조상클래스의 생성자 Product(int price)를 호출한다.
super(100);// Tv의 가격을 100만원으로 한다.
}

public String toString() {// Object클래스의 toString()을 오버라이딩한다.
return "Tv";
}
}

class Computer extends Product {
Computer() {
super(200);
}

public String toString() {
return "Computer";
}
}

class Buyer {// 고객, 물건을 사는 사람
int money = 1000;// 소유금액
int bonusPoint = 0;// 보너스점수

void buy(Product p) {
if(money < p.price) {
System.out.println("잔액이 부족하여 물건을 살수 없습니다.");
return;
}

money -= p.price;// 가진 돈에서 구입한 제품의 가격을 뺀다.
bonusPoint += p.bonusPoint;// 제품의 보너스 점수를 추가한다.
System.out.println(p + "을/를 구입하셨습니다.");
}
}

class PolyArgumentTest {
public static void main(String args[]) {
Buyer b = new Buyer();
Tv tv = new Tv();
Computer com = new Computer();

b.buy(tv);
b.buy(com);

System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다.");
}
}
실행결과)
Tv을/를 구입하셨습니다.
Computer을/를 구입하셨습니다.
현재 남은 돈은 700만원입니다.
현재 보너스점수는 30점입니다.
cf.)
Print(Object o)는 매개변수로 Object타입의 변수가 선언되어 있는데 Object 클래스는 모든 클래스의 조상이므로 이 메서드의 매개변수로 어떤 타입의 인스턴스도 가능하므로, 매개변수에 toString()을 호출하여 문자열을 얻어서 출력한다.