자바의 정석 Chapter 07 객체 지향 프로그래밍 Ⅱ - 05. 다형성편

5. 다형성(polymorphism)

5.1 다형성이란?

상속과 함께 객체지향개념의 중요한 특징 중에서 하나이다.
다형성은 상속과 깊은 관계가 있으므로 상속에 대해서 충분히 알고 있는 것이 좋다.

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

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

class TV {
	boolean power; // 전원상태 (on/off)
    int channel; // 채널
    
    void power() { power =!power; }
    void channelU() { ++channerl; }
    void channelDown() { --channel; }
}
class CaptionTv extends TV {
	String text; // 캡션을 보여 주기 위한 문자열
    void caption() { /*내용생략*/ }
}

Tv 클래스와 CaptionTv 클래스는 서로 상속관계에 있으며,
이 두 클래스의 인스턴스를 생성하고 상죵하기 위해서는 다음과 같이 할 수 있다.

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

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

즉, Tv인스턴스를 다루기 위해서는 Tv타입의 참조변수를 사용하고
CaptionTv인스턴스를 다루기 위해서는 CaptionTv타입의 참조변수를 사용했다.

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

Tv t = new CaptionTv(); // 조상 타입의 참조변수로 자손 인스턴스를 참조

그러면 인스턴스를 같은 타입의 참조변수로 참조하는 것과
조상타입의 참조변수로 참조하는 것은 어떤 차이가 있는지 알아보자.

CpationTv c = new CaptionTv();
TV t = new CaptionTV();

클래스 TV와 CaptionTV는 서로 상속관계에 있으며,
이 두 클래스의 인스턴스를 생성하고 사용하기 위해서는 다음과 같이 할 수 있다.

TV t = new TV();
caption TV c = new CaptionTV();

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

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

TV t = new captionTV(); // 조상 타입의 참조변수로 자손 인스턴스를 참조

인스턴스를 같은 타입의 참조변수로 참조하는 것과
조상타입의 참조변수로 참조하는 것은 어떤 차이가 있을까?

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

CaptionTV 인스턴스 2개를 생성하고 참조변수 c와 t가 생성된 인스턴스를 하나씩 참조했다.
이 경우 실제 인스턴스가 CaptionTV타입이라도 참조변수 t로 CaptionTV인스턴스의 모든 멤버를 사용할 수 없다.

TV 타입의 참조변수로는 CaptionTV 인스턴스 중에서 TV클래스의 멤버들(상속받은 멤버 포함)만 사용할 수 있다.

따라서, 생성된 CaptionTV인스턴스의 멤버 중에서 TV클래스에 정의되지 않은 멤버,
text와 caption()은 참조변수 t로 사용이 불가능하다.
즉, t.text 또는 t.caption()와 같이 할 수 없다는 것이다.
둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.

반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것도 불가능하다.

CaptionTV c = new TV();

컴파일 에러가 발생한다.

실제 인스턴스인 TV의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수보다 더 많기 때문이다.
그래서 이를 허용하지 않는 것이다.

CaptionTV클래스에는 text와 caption()이 정의되어 있으므로
참조변수 c로는 c.text, c.caption()과 같은 방식으로 c가 참조하고 있는 인스턴스에서 text와 caption()을 사용할 수 있다.

하지만 c가 참조하고 있는 인스턴스는 TV타입이고,
TV타입의 인스턴스에는 text와 caption()이 존재하지 않기 때문에 이를 사용하려고 하면 문제가 생긴다.

그래서 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은
존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용되지 않는 것이다.
참조변수가 사용할 수 있는 메버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.
! 클래스는 상속을 통해서 확장될 수는 있어도 축소될 수는 없어서,
조상 인스턴스의 멤버 개수는 자손 인스턴스의 멤버 개수보다 항상 적거나 같다.

참조변수의 타입이 참조변수가 참조하고 있는
인스턴스에서 사용할 수 있는 멤버의 개수를 결정한다는 사실을 이해하는 것은 아주 중요하다.
! 모든 참조변수는 null 또는 4byte의 주소값이 저장되며,
참조변수의 타입은 참조할 수 있는 객체의 종류와 사용할 수 있는 멤버의 수를 결정한다.

조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.

5.2 참조변수의 형변환

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

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

다운 캐스팅 : 조상타입의 참조변수 -> 자손타입의 참조변수
업캐스팅 : 자손타입의 참조변수 -> 조상타입의 참조변수

참조변수간의 형변환 역시 캐스트연산자를 사용하며, 괄호()안에 변환하고자 하는 타입의 이름(클래스명)을 적어주면 된다.

5.3 instanceof연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다.
주로 조건문에 사용되며 instanceof의 왼쪽에 참조변수를 오른쪽에 타입(클래스명)이 피연산자로 위치한다.
그리고 연산의 결과로 boolean값인 true와 false 중에서 하나를 반환한다.
instanceof를 이용한 연산결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 말한다.
_! 값이 null인 참조변수에 대해서 instanceof연산을 수행하면 false를 결과로 얻는다.

void doWork(Car c) {
	if (c instance of FireEngine) {
    		FireEngine fe = (FireEngine) c;
            fe.water();
            	...
    } else if (c instanceof Ambulance a = () {
    	Ambulance a = (Ambulance)c;
        a.sire();
        	...
    }
    ...
}

Car타입의 참조변수 c를 매개변수로 하는 메서드다.
이 메서드가 호출될 때 매개변수로 Car클래스 또는 그 자손 클래스의 인스턴스를 넘겨받겠지만
메서드 내에서는 정확히 어떤 인스턴스인지 알 수 없다.
그래서 instanceof연산자를 이용해서 참조변수 c가 가리키고 있는 인스턴스의 타입을 체크하고,
적절하게 형변환한 다음에 작업해야 한다.

조상타입의 참조변수로 자손타입의 인슽턴스를 참조할 수 있기에,
참조변수의 타입과 인스턴스의 타입이 항상 일치하지 않는다는 것을 배웠다.

조상타입의 참조변수로는 실제 인스턴스의 멤버들을 모두 사용할 수 없기에 실제 인스턴스와 같은 타입의 참조변수로
형변환을 해야만 인스턴스의 모든 멤버들을 사용할 수 있다.

5.4 참조변수와 인스턴스의 연결

조상 타입의 참조변수와 자손 타입의 참조변수의 차이점은 사용할 수 있는 멤버의 개수에 있다고 배웠다.
여기에 한 가지를 더하자면
조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스 변수를 자손 클래스에 중복으로 정의했을 때,
조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와
자손타입의 참조변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.

메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이
항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
_! static메서드는 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와 c 모두 Child 인스턴스를 참조하고 있다.
그리고 Parent 클래스와 Child클래스는 서로 같은 멤버들을 정의하고 있다.

이 때 조상타입의 참조변수 p로 Child 인스턴스의 멤버들을 사용하는 것과
자손타입의 참조변수 c로 Child인스턴스의 멤버들을 사용하는 것의 차이를 알 수 있다.

메서드인 method()의 경우 참조변수의 타입에 관계없이
항상 실제 인스턴스의 타입인 Child클래스에 정의된 메서드가 호출되지만,
인스턴스 변수인 x는 참조변수의 타입에 따라 달라진다.

5.5 매개변수의 다형성

참조변수의 다형적인 특지은 메서드의 매개변수에도 적용된다.
아래의 코드처럼 Product, Tv, Computer, Audio, Buyer 클래스가 정의되어 있다고 하자.

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, Audio, Compuer 클래스의 조상이며
Buyer클래스는 제품(Product)을 구입하는 사람을 클래스로 표현한 것이다.

Buyer클래스에 물건을 구입하는 기능의 메서드를 추가한다.
구입할 대상이 필요하므로 매개변수로 구입할 제품을 넘겨받아야 한다.
TV를 살수 있도록 매개변수를 TV타입으로 했다.

void buy(TV t) {
	// Buyer가 가진 돈(money)에서 제품의 가격(t.price)만큼 뺀다.
    money = money - t.price;

	// Buyer의 보너스점수(bonusPoint)에 제품의 보너스 점수(t.bonusPoint)를 더한다.
    bonusPoint = bonusPoint + t.bonusPoint;
}

buy(Tv t)는 제품을 구입하면 제품을 구입한 사람이 가진 돈에서 제품의 가격을 빼고,
보너스 점수는 추가하는 작업을 했다.

그런데 buy(Tv t)로는 TV만 살수 있으므로 다른 제품도 구입할 수 있는 메서드를 추가했다.

void buy(Computer c) {
	money = money - c.price;
    bonusPoint = bonusPoint + c.bonusPoint;
}

void buy(Audio c) {
	money = money - a.price;
    bonusPoint = bonusPoint + a.bonusPoint;
}

이렇게 하면 제품의 종류가 늘어날 때마다 Buyer클래스에는 새로운 buy메서드를 추가해줘야 할 것이다.
하지만 메서드의 매개변수에 다형성을 적용하면 아래의 코드처럼 하나의 메서드로 간단히 처리할 수 있다.

void buy(Product p) {
	money = money - p.price;
    bonusPoint = bonusPoint + p.bonusPoint;
}

매개변수 Product타입의 참조변수라는 것은,
메서드의 매개변수로 Product클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 것이다.

그리고 Product클래스에 price와 bonusPoint가 선언되어 있기에
참조변수 p로 인스턴스의 price와 bonusPoint를 사용할 수 있기에 이와 같이 할 수 있다.

앞으로 다른 제품 클래스를 추가할 때 Product클래스를 상속받기만 하면
buy(Product p)메서드의 매개변수로 받아들여질 수 있다.

Buyer b = new Buyer();
TV t = new Tv();
Computer c = new Computer();
b.buy(t);
b.buy(c);

_! TV t = new TV();b.buy(t);를 한 문장으로 하면 b.bu(newTv());가 된다.

TV 클래스와 Compuer클래스는 Product클래스의 자손이므로
위의 코드처럼 buy(Product p)메서드에 매개변수로 TV인스턴스와 Compuer인스턴스를 제공하는 것이 가능하다.

5.6 여러 종류의 객체를 배열로 다루기

조상타입의 참조변수로 자손타입의 객체를 참조하는 것이 가능하므로
Product클래스가 TV, Computer, Audio클래스의 조상일 때, 다음과 같이 할 수 있는 것을 배웠다.

Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();

위 코드를 Product타입의 참조변수 배열로 처리하면 아래의 코드처럼 된다.

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio;

이처럼 조상타입의 참조변수 배열을 사용하면,
공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.

또는 묶어서 다루고 싶은 객체들의 상속관계를 따져서
가장 가까운 공통조상 클래스 타입의 참조변수 배열을 생성하여 객체들을 저장하면 된다.
이런 특징을 이용해서 앞서 다룬 Buyer클래스에 구입한 제품을 저장하기 위한 Product배열을 추가한다.

class Buyer {
	int money = 1000;
    int bonusPoint = 0;
    Product[] item = new Product[10] // 구입한 제품을 저장하기 위한 배열
    int i = 0; // Product배열 item에 사용될 index
    
    void buy(Product p) {
    	if(money < p.price)
        	System.out.println("잔액이 부족해서 물건을 살 수 없습니다.");
            return;
    }
    money -= p.price; // 가진 돈에서 제품 가격을 뺀다.
    bonusPoint += p.bonusPoint; // 제품의 보너스포인트를 더한다.
    item[i++] = p; // 제품을 Produc[] item에 저장한다.
    Systme.out.println(p + "을/를 구입했습니다.");
}

구입한 제품을 담기 위해 Buyer클래스에 Product배열인 item을 추가했다.
그리고 buy메서드에 'item[i++] = p;'문장을 추가함으로써 물건을 구입하면, 배열 itemp에 저장하도록 했다.

이렇게 해서 모든 제품 클래스의 조상인 Product클래스 타입의 배열을 사용함으로써
구입한 제품을 하나의 배열로 간단하게 다룰 수 있게 된다.

좋은 웹페이지 즐겨찾기