FoO의 개발 블로그

다형성: Polymorphism 본문

Programming/Java

다형성: Polymorphism

FoO__511 2023. 10. 17. 14:03

다형성

다형성이라는 말은 이번에 처음 들어봤지만 생각보다 이해하기 쉬웠다. 짧게 말하면 어떤 객체를 다른 객체로 선언할 수 있다는 것이다.

 

Student bonbon = new Student();

Person bonbon = new Student();

 

위처럼 객체를 Person 타입으로 선언했지만 Student로 초기화할 수 있다. 대박~ 그런데 다 되는 것은 아니다. ㄱ-

위키백과의 다형성 정의는 이렇다.

프로그램 언어의 다형성(多形性, polymorphism; 폴리모피즘)은 그 프로그래밍 언어의 자료형 체계의 성질을 나타내는 것으로, 프로그램 언어의 각 요소들(상수, 변수, 식, 오브젝트, 함수, 메소드 등)이 다양한 자료형(type)에 속하는 것이 허가되는 성질을 가리킨다. 반댓말은 단형성(monomorphism)으로, 프로그램 언어의 각 요소가 한가지 형태만 가지는 성질을 가리킨다.

 

요컨대 각 요소가 다양한 객체가 될 수 있다는 것이다. 직접 다형성을 띠는 객체를 만들어보자.

 

다형성을 띠는 객체

선언과 초기화

위에서 말했듯 아무 객체로 선언해서 아무 객체로 초기화할 수 있는건 아니다. 부모클래스로 선언한 변수에 자식클래스로 초기화가 가능하다. 그 반대의 경우는 불가능하다.

 

[부모클래스] [변수명] = new [자식클래스]();

 

좀 더 구체적으로 예를 들자면:

 

class Animal{}
class Dog extends Animal{}

public class Blog {
    public static void main(String[] args){

        Animal littleDog = new Dog();

    }
}

 

그렇다면 이 객체는 자식클래스에서 추가된 클래스와 메서드에 접근할 수 있을까? 자식클래스에서 메서드를 override한 경우에는 부모클래서의 메서드를 사용할까 자식클래스의 메서드를 사용할까?

 

 

필드와 메서드

 

테스트를 해보기 위해 먼저 부모 클래스에서 필드 1개와 메서드를 2개 만들어보자.

 

class Animal{
    String name="littleAnimal";

    Animal(){}
    Animal(String name){
        this.name = name;
    }

    void eat(){System.out.println("Animal is eating..");}
    void drink(){System.out.println("Animal is drinking..");}
}

 

자식클래스에서는 새로운 필드와 메서드를 하나씩 추가하고 부모클래스의 메서드 중 하나를 override한다.

 

class Dog extends Animal{
    int age = 10;

    Dog(){}
    Dog(String name, int age){
        super(name);
        this.age = age;
    }

    void drink(){System.out.println("Dog is drinking..");}
    void sniff(){System.out.println("Dog is sniffing..");}
}

 

이제 메인 메서드에서 새 객체의 필드와 메서드에 접근해보자.

 

public class Blog {
    public static void main(String[] args){
        Animal littleDog = new Dog();

        // 필드에 접근
        System.out.println(littleDog.name);
        System.out.println(littleDog.age);

        // 메서드에 접근
        littleDog.eat();
        littleDog.drink();
        littleDog.sniff();
    }
}

 

위 코드를 실행하면 에러가 발생한다.

 

Blog.java:32: error: cannot find symbol
        System.out.println(littleDog.age);
                                    ^
  symbol:   variable age
  location: variable littleDog of type Animal
Blog.java:37: error: cannot find symbol
        littleDog.sniff();
                 ^
  symbol:   method sniff()
  location: variable littleDog of type Animal
2 errors

 

부모객체에 없는 필드와 메서드인 age, sniff()를 찾을 수 없다고 한다.

typeAnimal이 되면서 부모인 Animal에 없는 필드와 메서드에 접근할 수 없어지는 것이다.

오류가 나는 부분을 주석처리하고 실행했을 때의 결과는 이렇다.

 

littleAnimal
Animal is eating..
Dog is drinking..

 

자식클래스에서 override한 메서드가 실행된다.

 

위의 의문에 대한 해답

  • 그렇다면 이 객체는 자식클래스에서 추가된 클래스와 메서드에 접근할 수 있을까?
    -> 접근 불가
  • 자식클래스에서 메서드를 override한 경우에는 부모클래서의 메서드를 사용할까 자식클래스의 메서드를 사용할까?
    -> override한 메서드 사용

 

자식클래스 고유 필드와 메서드에 접근하려면

 

그런데 Dog클래스라는 객체를 만들고 littleDog라는 변수에는 그 주소만 저장된다. 그러면 Dog클래스에만 있는 필드와 메서드도 메모리 어딘가에 있어야 하는것 아닌가?

그렇다면 그 필드와 메서드를 사용하려면 어떡해야할까?

변수의 타입을 바꿔주면(캐스트) 된다.

 

public class Blog {
    public static void main(String[] args){
        Animal littleDog = new Dog("bigDog", 15);

        // 필드에 접근
        System.out.println(littleDog.name);

        // 캐스드
        System.out.println(((Dog)littleDog).age);

        // 메서드에 접근
        littleDog.eat();
        littleDog.drink();

        // 캐스트
        ((Dog)littleDog).sniff();
    }
}

 

결과:

 

bigDog
15
Animal is eating..
Dog is drinking..
Dog is sniffing..

 

다형성이 생기는 원리

 

다시 위의 예를 보자.

 

class Animal{}
class Dog extends Animal{}

public class Blog {
    public static void main(String[] args){

        Animal littleDog = new Dog();

    }
}

 

이 예에서 littleDog자료형은 Animal이다. 하지만 초기화는 Dog 클래스로 되어서 속은 Dog이다.

 

인간 친화적으로 설명하면

개는 항상 동물이므로 상위분류인 동물이라고 해도 된다.
하지만 동물은 개일수도 있고 고양이일 수도 있으므로 그 하위분류인 개나 고양이라고 섣불리 말할 순 없다.

 

컴퓨터 친화적으로 설명하자면

 

(사실 이건 여기저기 찾아보며 도출해낸 추측이다.. 틀렸을 수도 있다ㅋ 정정 환영합니다!! 제발 알려주세요!)

객체를 생성하면 메서드는 코드 영역, 객체 자체는 힙 영역, 선언한 변수는 객체가 있는 힙 영역의 주소로서 스택 영역에 저장된다. 필드는 원시 데이터타입의 경우 코드 영역, 객체일 경우 힙 영역에 저장된다.

자식객체가 생성되면서 자식객체가 가지고 있는 모든 메서드는 코드 영역에, 모든 필드는 종류에 따라 코드 영역과 힙 영역에 올라간다. 인스턴스 자체는 힙 영역에 올라간다.

클래스 내부에서 초기화한 기본 필드값은 자식쪽에서 재선언하더라도 부모의 것으로 저장되고, 메서드는 자식측에서 override한 것이 저장된다. 왜인지는 모르겠다. 자식객체만 가지고 있는 필드와 메서드도 당연히 메모리에 올라간다.

이제 객체 생성이 끝났고, 객체 주소를 변수에 저장하고 그 변수의 타입을 지정한다. 자바는 그 타입에 따라 변수에서 접근할 수 있는 필드와 메서드들을 판단한다. 타입이 다르므로 자식클래스에만 있는 필드와 메서드에는 접근할 수 없지만, 캐스트를 통해 타입을 바꿔주면 접근 가능해진다.

 

다형성 활용하기

 

그러면 이 다형성을 어디 써먹어야 할까?

 

배열

 

다형성을 배열에 이용하면 편하다. 파이썬같은 고레벨 언어와는 달리 자바나 C/C++같은 언어들의 배열은 한가지 타입만 사용할 수 있다.

작업을 하다가 개 클래스도 만들고 고양이 클래스도 만들고 토끼 클래스도 만들었는데, 배열 하나에 넣어 관리하고 싶어도 그러지 못한다. 하지만 다형성을 활용하면 다른 타입의 클래스들도 한 배열 안에 넣을 수 있다!

 

class Animal{}

class Dog extends Animal{}
class Cat extends Animal{}
class Rabit extends Animal{}


public class Blog {
    public static void main(String[] args){
        Animal[] cuteBuddies = new Animal[10];

        cuteBuddies[0] = new Dog();
        cuteBuddies[1] = new Cat();
        cuteBuddies[2] = new Rabit();

        for (Animal animal: cuteBuddies){
            System.out.print(animal + "  ");
        }

    }
}

 

결과

 

Dog@2eafffde  Cat@59690aa4  Rabit@6842775d  null  null  null  null  null  null  null

 

매개변수

 

자바 프로그래밍을 하며 매개변수의 타입을 메서드 정의와 다르게 넣으면 오류가 발생한다. 이때 모든 클래스의 부모인 Object 를 매개변수의 타입으로 사용하면 아무 값이나 넣어도 된다.

 

Object 클래스 공식문서

 

class Dog{}

public class Blog {
    static void putAnything(Object imAnything){System.out.println(imAnything);}

    public static void main(String[] args){
        putAnything(1);
        putAnything("가나다");
        putAnything(new Dog());
    }
}

 

결과:

 

1
가나다
Dog@41a4555e

 

끝내며

 

JVM을 리버싱할 능력이 안되니 실제로 메모리에서 무슨 일이 일어나는지 확실히는 모르겠으나, 이 정도만으로도 다형성을 활용하기에는 문제가 없을 것 같다.

Object클래스에 대해 자세히 공부하면 좋을 듯 하다.