[JAVA] 상속, 인터페이스에 의한 다형성(polymorphism)

2019. 10. 7. 11:54 JAVA/Java

상속, 인터페이스에 의한 다형성(polymorphism)




1. 지연 바인딩


보통은 변수의 타입은 이 변수가 참조하는 개체의 클래스와 일치한다.


  ChessPiece bishop = new ChessPiece();


위에서 bishop 변수는 ChessPiece 클래스를 실체화시킴으로써 생성되는 객체를 가리키는데 사용될 수 있다.

이렇게 변수의 타입과 이 변수가 참조하는 객체가 호환 가능해야 하나 정확하게 일치할 필요는 없다.


참조 변수와 이 변수가 참조하는 객체와의 관계는 더 유연하다.


다형성(polymorphism)이란 용어는 "여러 형태를 갖는 것"으로 정의된다.

다형 참조(polymorphic reference)는 때에 따라 다른 타입의 객체를 참조할 수 있다는 것이다. 다형 참조를 통해서 호출되는 특정 메소드는 호출간에 다를 수 있다.


obj.doIt();


obj 참조 변수가 다형적이면, 이 참조는 다른 시기에 다른 타입의 객체를 참조할 수 있다.

위의 코드가 for/while 문과 같은 루프 내부에 포함되어 있거나 한번 이상 호출되는 메소드 내부에 포함되어 있으면, 위의 코드는 doIt 메소드가 호출될 때마다 그 메소드의 다른 버전을 호출할 수 있을 것이다.


어떤 시점에서, 메소드가 호출될 때 어떤 코드를 수행할 것인지가 결정된다. 이러한 결정을 메소드 호출을 메소드 정의에 바인딩(binding)한다고 한다.

많은 경우에, 메소드 정의에 대한 메소드 호출의 바인딩이 컴파일 시간에 일어날 수 있다. 

그러나 다형 참조의 경우에는 실행 시간까지 바인딩을 할 수 없다.

사용되ㅗ는 메소드 정의는 호출 순간에 참조 변수에 의해서 참조도괴 있는 객체에 기초한다. 이렇게 늦게 결정하는 것을 지연 바인딩(late binding) 또는 동적 바인딩(dynamic binding)이라고 한다.


Java에서 상속을 이용하는 것과 인터페이스를 이용하는 두 가지 방식으로 다형 참조를 생성할 수 있다.


2. 상속에 의한 다형성


Animal creature = new Horse();


Animal 이라는 클래스 타입으로 변수를 선언하면 이 변수는 Animal 의 어떤 객체라도 참조할 수 있다.

추가로, Animal 을 상속한 어느 클래스의 어느 객체라도 참조할 수 있다.

따라서 Animal 을 상속한 Horse() 객체를 참조할 수 있다.


클래스 계층구조의  최상단까지 적용하면, Object 참조 변수는 궁극적으로 모든 클래스가 Object 클래스의 자손ㅇ이기 때문에 어떤 객체든 가리킬 수 있다.


creature 는 Animal, Horse, 또는 Animal을 상속한 어떤 클래스도 참조할 수 있기 때문에 다형적이다. 이 클래스들이 모두 다른 방식으로 구현된 move 라는 메소드를 가지면 아래 호출은 해당 참조 클래스의 move() 메소드를 호출한다.


creature.move();


그러나 호출되는 메소드가 어떤 것일지는 실행시간에 결정된다. (지연 바인딩)


위 문장이 실행될 때, creature가 현재 Animal 객체를 참조하고 있다면 Animal 클래스에 속한 move() 메소드가 호출된다.

마찬가지로 Horse 객체를 참조하고 있다면 Horse 객체의 move() 메소드가 호출된다.


이렇게 상속에 따른 다형의 객체를 참조할 수 있으며, 메소드 호출 또한 실행 당시의 참조 객체의 메소드를 선택하여 호출하는 것을 상속에 의한 다형성이라고 한다.



3. 인터페이스에 의한 다형성


인터페이스 역시 클래스와 마찬가지로 변수 타입으로 사용될 수 있다.


public interface Speaker{
    
    public void speak();
    public void announce(String str);

}


인터페이스 이름, Speaker는 이제 다음과 같이 객체 참조 변수를 선언하는데 사용할 수 있다.


Speaker current;


변수 current는 Speaker 인터페이스를 구현하는 어떤 클래스의 아무 객체라도 참조하는데 사용할 수 있다.

예를들어, Philosopher 라는 Speaker 인터페이스를 구현하는 클래스가 있다면, 다음과 같이 Philosopher 객체를 Speaker 참조 변수에 배정할 수 있다.


current = new Philosopher();


이것은 Philosopher가 Speaker이기 때문에 적법하다. 이러한 점에서, 클래스와 인터페이스 간의 관계는 자식 클래스와 그 부모 클래스 간의 관계와 동일하다.  이것은 is-a 관계이다. 이러한 관계가 다형성의 기초를 형성한다.


인터페이스 참조의 유연성은 다형 참조를 생성하는 것을 허용한다. 앞서 보았듯이, 상속을 사용하여 객체들의 집합에 속한 아무 객체라도 참조할 수 있는 다형 참조를 생성할 수 있다.

물론, 그 집합에 포함된 객체들은 상속으로 연관되어 있어야 한다. 인터페이스를 사용하여 동일한 인터페이스를 구현하는 객체들에 대해서 유사한 다형 참조를 생성할 수 있다.


예를들면, Speaker 인터페이스를 구현하는 Dog 라는 클래스를 생성하면, Dog 객체도 Speaker 참조 변수에 배정될 수 있다.


Speaker guest;
guest = new Philosopher();
guest.speak();

guest = new Dog();
guest.speak()


위의 코드에서, speak() 가 처음 호출될 때, Philosopher 클래스에 정의되어 있는 speak 메소드가 호출된다. speak 메소드가 두 번째로 호출될 때는 Dog 클래스의 speak 메소드가 호출된다. 

상속에 의한 다형 참조처럼, 어느 메소드가 호출될 것인지를 결정하는 것은 참조 변수의 타입이 아니다. 이것은 메소드 호출 순간에 참조 변수가 가리키고 있는 객체의 타입에 따른다.



인터페이스 참조 변수를 사용하고 있을 때, 그 참조 변수가 가리키고 있는 객체가 인터페이스에 없는 다른 메소드를  포함하고 있다고 할지라도, 그 인터페이스에 정의되어 있는 메소드만을 호출할 수 있다.


예를들어, Philosopher 클래스가 pontificate 라는 메소드도 정의했다고 가정하면 아래 코드는 컴파일 오류를 발생시킨다.


Speaker special = new Philosopher();
special.pontificate();


이 문제는 컴파일러는 단지 그 객체가 Speaker 라는 것만을 결정할 수 있고, 그 객체가 Speaker 인터페이스의 speak() 와 announce() 메소드만 사용할 수 있다는 것만 보장하기 때문이다.


참조변수 special 이 Dog 객체를 참조한다면 컴파일러는 그 pontificate() 메소드 호출을 할 수 없다고 오류를 발생한다.


특별한 상황에서 pontificate() 메소드 호출이 적법하며 호출이 필요할때는 적절한 참조 변수로 캐스트하여 컴파일러가 허용할 수 있게 할 수 있다.


Speaker special = new Philosopher();
((Philosopher)special).pontificate();



상속에 기초한 다형 참조 변수처럼, 인터페이스 이름을 메소드 매개변수의 타입으로 사용할 수 있다. 이러한 상황에서, 인터페이스를 구현하는 어느 클래스의 어느 객체라도 그 메소드에 전달될 수 있다.


public void sayIt(Speaker current){
    
    current.speak();
    
}



다형 참조를 메소드의 형식 매개변수로 사용하는 것은 강력한 기법이다.

이것은 메소드가 전달되는 매개변수의 타입을 제어하는 것을 허용하고, 메소드에게 다양한 타입의 인자들을 받아들일 수 있는 유연성을 제공한다.



출처: https://hyeonstorage.tistory.com/266?category=557602 [개발이 하고 싶어요]