프로그래밍 언어/Java

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

happy_life 2022. 6. 24. 12:12

목차

1. 다형성이란?

2. 참조변수간 형변환

3. instanceof 연산자

4. 상속과 다형성 활용해보기

 

 

1. 다형성이란?

객체 지향에서 가장 중요한 개념 중의 하나는 바로 다형성이다. 다형성이 무엇일까?

다형성은 하나의 객체가 여러 타입을 가질 수 있는 성질을 의미한다.

이는 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 하여 실제 코드로 구현하고 있다.

코드 예제를 통해 알아보자.

 

다형성 코드 예제 

public class PolymorphismEx1 {
    public static void main(String[] args) {
        // 다형성 X
        Parent parent = new Parent();

        // 다형성 O 부모가 자식을 참조 가능
        Parent a = new Child();
        
        // 자식이 부모 참조 불가능
       // Child b = new Parent();
    }
}
class Parent {
    int x = 5;
    int y = 10;
    void method() {
        System.out.println("부모입니다.");
    }
    
}

class Child extends Parent {
    int z = 15;
    void method() {
        System.out.println("자식입니다");
    }
}

부모는 자식 타입을 참조가능하지만 주의할 점이 있다. 이렇게 캐스팅한 경우, 멤버 변수에 관해서는 자식범위에 접근할 수 없다. (메서드는 자식범위 접근 가능) 부모 참조 타입으로 선언하는 것은 실제로는 자식이지만, 변수만큼은 부모 범위까지만 접근하겠다는 약속이다.

위의 경우 child의 z는 참조할 수 없을 것이다. 만약 참조하려 하는 경우 아래처럼 에러가 발생한다.

 

 

2. 참조변수간 형변환

참조변수 또한 형변환이 가능하다. 단 서로 상속관계에 있는 클래스 사이에서만 가능하다. 자식타입의 참조변수를 부모타입의 참조변수로, 부모타입의 참조변수를 자식타입의 참조변수로 형변화이 가능하다.

 

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

자식 -> 부모 형변환 생략 가능
부모 -> 자식 형변환 생략 불가능

 

코드 예제

// 자식 -> 부모 (형변환 생략 가능)

Parent parent = new Parent();
Child child = new Child();

// 자식 -> 부모 (형변환 생략 가능)
parent = child;
parent.method();
// 자식입니다

parent가 0x100의 주소

child가 0x200의 주소를 가리킨다고 생각해보자.

parent = child 코드를 통해 0x200의 주소가 parent 참조변수에 들어갈 것이다.

그러면 parent는 0x200의 child 객체를 가리키게 될 것이다. 하지만 Parent형이기 때문에 parent에 있는 멤버만 접근할 수있다.

하지만,  메서드의 경우 기존에 자식타입이었으므로, 자식 메서드에 접근하게 된다.

 

요약:

1. 부모로 형변환하면 형변환 생략가능

2. 멤버는 부모 범위만

3. 메서드는 자식범위 

 

 

 

// 부모 -> 자식 (형변환 불가능)

Parent parent = new Parent();
Child child = new Child();

// 부모 -> 자식 (형변환 필수)
child = (Child)parent;

부모 -> 자식의 형변환으로 형변환은 가능하다. 컴파일까지는 가능하지만, 이 코드는 ClassCastException이 발생한다.

자식 타입의 참조변수에 부모 타입을 캐스팅할 수 없기 때문이다. 자식은 부모가 갖지 못한 멤버와 메서드를 갖고 있을 수 있기 때문에 자식에 부모를 캐스팅할 수 없다고 배웠었다는 것을 다시 기억해보자.

 

이제 다시 // 부모 -> 자식 을 캐스팅하는 코드를 보자.

// 부모 -> 자식 (형변환 필수)
child = (Child)parent;

자식에 부모를 캐스팅하고 있는 모양이다.  따라서 오류가 발생한 것이다.

 

서로 상속관계에 있는 타입간의 형변환은 자유롭다. 하지만 참조변수가 가리키는 인스턴스의 자식 타입으로의 변환은 허용되지 않는다.  즉 실제로 변수가 어떤 타입인지를 체크하는 것이 중요하게 되는 것이다.

 

 

3. instanceof 연산자

참조변수가 참조하고 있는 실제 타입을 알기 위해 instanceof 연산자가 있다. instanceof 의 결과로 True를 얻으면, 참조변수가 검사한 타입으로 형변환이 가능하다는 의미이다. 이를 적용해보자

 

instanceof 코드예제

public class PolymorphismEx3 {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent parent2 = new Child();
        Child child = new Child();

        System.out.printf("parent instanceof Parent = %b\n", parent instanceof Child);
        //parent instanceof Parent = false
        System.out.printf("parent instanceof Parent = %b\n", parent2 instanceof Child);
        //parent instanceof Parent = true

        // 자식 타입으로 형변환 해보기
//        parent = (Child) parent; // ClassCaseException 오류
        parent2 = (Child) parent2; // 가능 instanceof 가 true이므로
    }
}

parent2는 실제 타입이 Child이므로 instanceofChild의결과로 true값을 받는다. 따라서 형변환이 가능하다.

한편, parent2의 경우는 실제 Child타입임에도 instanceofParent로 해도 true의 결과를 받는다. 이유는 Child는 Parent의 멤버들을 상속받은 것이기 때문이다. 자식은 부모의 인스턴스도 포함하고 있는 것이다. super로 부모인스턴스를 참조하는 것을 떠올려보자.

 

 

 

4. 상속과 다형성 활용해보기

요구사항

1.책과 연필이라는 제품에는 가격 멤버가 있고 이를 구매하는 구매자가 있다.

2. 상속을 활용해 다형성 구현하기

 

 

코드예제

Product.class

public class Product {
    int price; //가격 멤버변수

    Product(int price) {
        this.price = price;
    }
}

 

 

Book.class

public class Book extends Product{
    Book(int price) {
        super(price);// 인자로 받은 가격로 부모 인스턴스를 초기화한다.
    }

    @Override
    public String toString() {
        return "책";
    }
}

Product.class를 상속받은 Book.class

Object의 toString()을 오버라이딩해 "책"을 반환 ( 객체를 참조하면 "책"을 return하게한다.)

 

 

Pencil.class

public class Pencil extends Product{
    Pencil(int price) {
        super(price); // 인자로 받은 가격로 부모 인스턴스를 초기화한다.
    }

    @Override
    public String toString() {
        return "연필";
    }
}

Product.class를 상속받은 Pencil.class

Object의 toString()을 오버라이딩해 "책"을 반환 ( 객체를 참조하면 "연필"을 return하게한다.)

 

 

Consumer.class

public class Consumer {
    int money = 10000;

    public void buy(Product product) {
        // 잔액 부족시
        if (this.money < product.price) {
            System.out.println("잔액이 부족합니다.");
            return;
        }
        this.money -= product.price;
        System.out.println(product + "를 구매하였습니다. 잔액: "+ this.money);
    }
}

잔액 부족시잔액이 부족하다는 것을 알리고 바로 return한다

잔액이 충분하다면 구매했음을 출력하고 남은 잔액을 표시하게 된다.

 

 

PolymorphismEx4.class

public class PolymorphismEx4 {
    public static void main(String[] args) {
        Book book = new Book(5000); // 5000원 짜리 book
        Pencil pencil = new Pencil(1000); // 1000원 짜리 pencil


        Consumer consumer = new Consumer(); // 소비자

        // 구매하기
        consumer.buy(book); //책를 구매하였습니다. 잔액: 5000
        consumer.buy(pencil);//연필를 구매하였습니다. 잔액: 4000
        consumer.buy(pencil);//연필를 구매하였습니다. 잔액: 3000
        consumer.buy(pencil);//연필를 구매하였습니다. 잔액: 2000
        consumer.buy(book);//잔액이 부족합니다.
    }
}

 

코드설명

1. 상속과 다형성을 활용한 부분

Consumer의 buy메서드는 인자로 Product를 받는다. Book과 Pencil은 Product를 상속받은 클래스이기 때문에 인자로 들어갈 수 있게 된다.  Product라는 부모 클래스가 자식 클래스를 참조할 수 있다는 다형성의 개념이 들어간것이다. 이 개념을 활용하면이렇게 다양한 객체를 하나의 입구를 통해 들어갈 수 있게 할 수 있다.

 

2. 부모의 멤버를 참조한 부분

자식 인스턴스가 생성되면 부모 인스턴스 또한 생성되어 같이 있는 것이다.  (super를 사용할 수 있는 이유도 이와 같다)

따라서 Book 인스턴스 생성의 경우 super(5000) 이라는 코드를 통해 초기화된 부모클래스의 price 참조할 수 있는 것이다.