본문 바로가기

Java

다형성(Polymorphism)

728x90

다형성이란?

객체지향개념에서 여러 가지 형태를 가질 수 있는 능력을 의미합니다. 즉, 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 하는 기능이라고 할 수 있습니다.


class Animal {
    int size, legCount;
    String type;
    
    void makeSound(){}
    
}

class Dog extends Animal{
    boolean bark;
}


Animal animal = new Dog(); // 조상 타입의 참조변수로 자손 객체 참조
Dog dog = new Dog();

 

위 코드에서, 같은 클래스의 객체를 가르치는 Animal형 변수와 Dog형 변수에는 어떤 차이가 있을까요? animal 변수가 참조할 수 있는 멤버는 Animal 클래스 내 멤버 변수와 메서드 뿐이지만, dog 변수가 참조할 수 있는 멤버는 Animal 클래스 내 멤버와 Dog 클래스 내 멤버 전체입니다.

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


그럼 아래와 같이 자식 클래스형 참조 변수로 부모 클래스의 객체를 참조할 수는 없을까요? 

Dog dog2 = new Animal();

이렇게 자손 클래스 타입의 참조변수로 부모 객체를 참조하는 것은 불가능합니다. 실제 인스턴스인 Animal의 멤버 개수보다 참조변수 dog2가 사용할 수 있는 멤버 개수가 더 많기 때문에, 위와 같이 선언할 경우 존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않습니다. 즉, 참조변수가 사용할 수 있는 멤버 개수는 인스턴스의 멤버 개수보다 같거나 적어야 합니다.

 

물론 아래와 같이 다운캐스팅을 이용하여 코드를 작성하면 컴파일에 성공할 수 있습니다.  

Dog dog2 = (Dog) new Animal();

하지만, 실행 시 에러(ClassCastException)가 발생하는데, 발생 이유는 형변환에 오류가 있기 때문입니다. 컴파일 시에는 참조변수 간의 타입만 체크하기 때문에 실행 시 생성될 인스턴스의 타입에 대해서는 전혀 알지 못해 통과하지만, 위에서도 배웠듯이 조상타입 객체를 자손타입의 참조변수로 참조하는 것은 허용되지 않으므로 실행 시 에러가 발생하여 프로그램이 비정상적으로 종료됩니다. 

 

즉, 다형성 개념을 통해서, 참조변수의 타입이 참조변수가 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 결정한다는 사실을 알고 있는 것이 매우 중요합니다. 또한, 참조변수가 참조하고 있는 인스턴스의 자손타입으로 형변환하는 것은 허용되지 않기 때문에 참조변수가 가리키는 인스턴스의 타입이 무엇인지 instanceof 연산자 등을 통해 꼭 확인해야 합니다.


다형성의 활용

다형성을 이용하면 기존에 여러 번 작성해야 했던 코드의 중복을 없애고 하나로 통합할 수 있습니다. 

예시 코드로 확인해 보겠습니다.


public class Product {
    int price;
    String type, color;

    Product(){}
    Product(int price, String type){
        this.price = price;
        this.type = type;
    }
    Product(int price, String type, String color){
        this(price, type);
        this.color=color;
    }

    void method() {
        System.out.println("What product do you want?");
    }

}

class Fruit extends Product{
    Fruit(int price, String type, String color){
        super(price, type ,color);
    }
    @Override
    void method(){
        System.out.printf("Buy %s\nThe price is: %d\n", color + " " + type, price);
    }

}

class Drink extends Product{
    Drink(int price, String type){
        super(price, type);
    }

    @Override
    void method(){
        System.out.println("I want to drink something.");
        System.out.printf("Buy %s\nThe price is :%d\n", type, price);
    }


}
class Buyer {
    /* void buy(Drink d){
        d.method();
    }
    void buy(Fruit f){
        f.method();
    }*/

    // 아래 하나로 통합
    void buy(Product p) {
        p.method();
    }
}


Buyer buyer = new Buyer();
buyer.buy(new Product());
System.out.println("----------------------------");
buyer.buy(new Drink(3000, "Chocolate Drink"));
System.out.println("----------------------------");
buyer.buy(new Fruit(10000, "Apple", "Red"));

 

혹은 아래와 같이 배열(리스트, 벡터 등)을 이용해서 간단하게 작성할 수 있어 다형성은 유용하게 사용될 수 있습니다.

Product[] pv = new Product[]{new Product(), new Drink(3000, "Chocolate Drink"), new Fruit(10000, "Apple", "Red")};
for(Product p: pv){
	System.out.println("----------------------------");
    buyer.buy(p);
 }

 

[실행결과]

What product do you want?
----------------------------
I want to drink something.
Buy Chocolate Drink
The price is :3000
----------------------------
Buy Red Apple
The price is: 10000

 

위 결과를 통해 다형성을 이용하여 자식 클래스의 인스턴스로 메서드를 호출하면, 각 자식 클래스에서 오버라이딩 된 메서드가 호출됨을 확인할 수 있습니다. 즉, 참조변수는 부모 클래스 타입이지만, 참조되는 객체의 타입은 자식 클래스이므로 자식 클래스에서 오버라이드 된 메서드를 호출할 수 있게 되는 것입니다.

 

이렇게 다형성은 abstract 클래스 및 메서드에도 매우 요긴하게 사용되기 때문에 잘 숙지해서 객체지향개념을 적재적소에 활용하면 좋을 것 같습니다.


 

여기까지가 다형성의 개념이었습니다. 방금 전 언급한 instaceof에 대해서도 정리해 보았습니다.


instanceof 

instanceof를 통해 해당 변수가 어떤 타입의 인스턴스인지 확인할 수 있습니다.

아래 예시를 통해서 확인해 보겠습니다.


class Car {
    String color;
    int door;

    void drive() {
        System.out.println("Car drive.");
    }

    void stop() {
        System.out.println("stop!!");
    }
}

class Bus extends Car {
    void station() {
        System.out.println("Bus station!");
    }

    @Override
    void drive() {
        System.out.println("Bus drive.");
    }
    
    @Override
    void stop(){
        System.out.println("Passengers want to take off.");
    }
}

 

아래와 같이 호출해 보겠습니다.


Car car = new Car();
car.drive();
car.stop();
System.out.println("----------------------------");

Car car2 = new Bus();
car2.drive();
car2.stop();
//  car2.station(); 사용 불가
System.out.println("----------------------------");

Bus bus = new Bus();
bus.drive();
bus.station();
System.out.println("----------------------------");

System.out.println();

if (bus instanceof Car) {
    System.out.println("my type is Car");
}
if (bus instanceof Bus) {
    System.out.println("my type is FireEngine");
}
if (bus instanceof Object) {
    System.out.println("my type is Object");
}

System.out.println(bus.getClass().getName());

 

[실행결과]

Car drive.
stop!!
----------------------------
Bus drive.
Passengers want to take off.
-----------------------------
Bus drive.
Bus station!

my type is Car
my type is FireEngine
my type is Object
com.example.demo.Bus

 

위 결과에서 확인할 수 있듯이,  Bus 클래스는 Car 클래스를 상속하고, Car 클래스는 Object 클래스를 상속하므로 3가지 if문은 모두 true입니다. instanceof 연산의 결과가 true라는 것은 검사한 타입으로 형변환을 해도 아무런 문제가 없다는 의미도 내재합니다.


instanceof는 보통 다른 메서드에서 매개변수로 넘어온 변수의 타입을 확인하여 특정 메서드를 사용할 수 있는지의 여부를 체크하고 실행하기 위해서 사용합니다. 아래의 코드에서 확인할 수 있습니다.


void checkInstance(Car c) {
       if (c instanceof Bus) {
           Bus bus = (Bus) c;
           bus.station();
       } else {
            c.drive();
      }
 }
 
 
 // 아래와 같이 호출
 checkInstance(new Car());
 checkInstance(new Bus());

[실행결과]

Car drive.
Bus station!

 

728x90
반응형

'Java' 카테고리의 다른 글

열거형(enums)  (2) 2023.10.21
Comparable과 Comparator  (4) 2023.10.20
추상클래스(Abstract)과 인터페이스(Interface)  (0) 2023.10.15
제어자 (static과 final)  (0) 2023.10.15
JVM의 메모리 구조  (0) 2023.10.14