JAVA

[JAVA] Polymorphism (다형성) 이용하여 여러 개의 객체를 구현하기

제이미로그 2024. 4. 17. 21:11

📌 1. 다형성이란? 

 

다형성(polymorphism)이란 부모-자식 상속 관계에 있는 클래스에서

상위 클래스가 동일한 메시지로 하위 클래스들을 서로 다르게 동작시키는 객체 지향 원리입니다. 

객체 지향 프로그래밍에서 하나의 인터페이스나 추상 클래스를 통해 여러 개의 구현체를 사용할 수 있는 기능을 말합니다.

부모 type의 참조변수로 자식 type의 객체를 가리킵니다. 부모의 멤버만 접근 가능하며, 단 오버라이딩된 자식의 멤버 메소드는

접근이 가능합니다. 

 

다형성을 활용하면 부모 클래스가 자식 클래스의 동작 방식을 알 수 없어도 오버라이딩을 통해 자식 클래스를 접근할 수 있습니다.

그렇다면 어떻게  부모가 자식이 어떤 일을 하는 지 몰라도,  자식 멤버 함수를 호출시킬 수 있을까요? 

이유는 동적 바인딩 때문입니다. 동적바인딩이란, 메서드가 실행 시점에서 성격이 결정되는 바인딩인데요.

프로그램의 컴파일 시점에 부모 클래스는 자신의 멤버 함수밖에 접근할 수 없으나,

실행 시점에 동적 바인딩이 일어나 부모클래스가 자식 클래스의 멤버함수를 접근하여 실행할 수 있습니다. 

 

📌 2. 다형성 장점 

 

1) 유지보수가 쉽다 

개발자가 여러 객체를 하나의 타입으로 관리가 가능하기 때문에 코드 관리가 편리해 유지보수가 용이합니다.  

2) 재사용성 증가

다형성을 활용하면 객체를 재사용하기 쉬워지기 때문에 개발자의 코드 재사용성이 높집니다.

3) 느슨한 결합

다형성을 활용하면 클래스간 의존성이 줄어들며 확장성이 높고 결합도가 낮아져 안전성이 높아집니다. 

 

📌 3. 다형성 필수 조건

 

 1) 상속 관계

다형성을 활용하기 위해서는 필수로 부모-자식 간 클래스 상속이 이루어져야 합니다. 

 2) 오버라이딩 필수 (자식 클래스에서 메소드 재정의) 

다형성이 보장되기 위해서는 하위 클래스 메소드가 반드시 재정의되어 있어야 합니다. 

 3) 업캐스팅 (Upcasting)

부모 클래스 타입의 참조변수로 자식 클래스의 객체를 참조할 수 있어야 합니다.

이를 통해 다양한 종류의 자식 클래스 객체를 부모 클래스 타입으로 다룰 수 있습니다. 

4) 실행 시점의 다형성 (Execution-time Polymorphism)

다형성은 실행 시에 객체의 실제 타입에 따라 메서드가 호출되는 것을 의미합니다.

따라서 객체가 실제로 어떤 클래스의 인스턴스인지에 따라 메서드 호출 결과가 달라집니다. 

 

📌 4. 다형성 구현 방법 

 

1) 상속 클래스 구현 (부모-자식 클래스 구현)

2) 메소드 오버라이딩 

3) 업캐스팅하여 객체 선언

4) 부모 클래스 객체로 자식 메소드 호출

 

📌 5. 다형성 예시 

 

1) 상속 클래스 구현 (부모-자식 클래스 구현)

- 부모 자식간 상속 클래스를 구현합니다.

- 부모 클래스는 Book 클래스이며 자식 클래스는 Novel 클래스입니다.

- 디폴트 생성자 외 인수 있는 생성자를 추가해 생성자를 중복 정의합니다.

 

 

 

2) 메소드 오버라이딩

- 메소드 오버라이딩(Override)은 상속 관계에 있는 클래스에서 부모 클래스의 메소드를 자식 클래스에서 재정의하는 것.

- 자식 클래스는 부모 클래스의 메소드를 그대로 사용하면서도 필요에 따라 자신의 동작을 변경할 수 있습니다. 

 

= > 메소드 오버라이딩의 조건

- 메소드 이름, 매개변수의 타입 및 개수, 반환 타입이 부모 클래스의 메소드와 동일해야 합니다. 

- 접근 제어자는 부모 클래스의 메소드보다 더 넓은 범위로 변경할 수 있습니다.

부모 클래스의 메소드가 'protected'로 선언되어 있다면 자식 클래스에서는 'protected' 또는 'public'으로 선언할 수 있습니다. 

- 예외를 선언할 때, 자식 클래스에서 선언하는 예외는 부모 클래스의 예외를 포함하거나 그 예외의 하위클래스여야 합니다.

 

 

- 자식 클래스인 'Novel'에서 부모 클래스 'Book'의 'print' 메소드를 오버라이딩하고 있습니다. @Override 어노테이션은 컴파일러에게

해당 메소드가 상위 클래스에서 상속된 메소드를 재정의한다는 것을 알려 줍니다. "print : Novel" 이라는 메시지를 출력하도록 'print' 

메소드를 재정의하고 있습니다. 

 

 

- SF 클래스가 Book 클래스를 상속하고 있으며, print 메소드를 오버라이딩하고 있습니다. SF 클래스의 생성자는 부모 클래스인 

Book 클래스의 생성자를 호출하고 있습니다. 따라서 SF 클래스의 print 메소드는 "print : SF" 라는 메시지를 출력하게 됩니다. 

이것은 Book 클래스에서 상속받은 'print' 메소드를 'SF' 클래스에서 새로운 동작으로 재정의한 것입니다.

 

3) 업캐스팅하여 객체 선언

- 업캐스팅(자식 클래스 객체를 부모 클래스로 형변환)하여 아래와 같이 객체를 선언합니다. 

 

- 'Novel' 과 'SF' 클래스는 모두 'Book' 클래스를 상속하고 있습니다.

- 'Book' 타입의 변수 'b'와 'c'는 'Novel'과 'SF' 클래스의 인스턴스를 가리킬 수 있습니다.

- 'b'는 'Novel' 클래스의 인스턴스를 가리킵니다.

- 'c'는 'SF' 클래스의 인스턴스를 가리킵니다.

= > 상위 클래스 타입의 변수로 하위 클래스의 인스턴스를 참조할 수 있게 만듭니다. 

 

4) 부모 클래스 객체로 자식 메소드 호출

- 부모 클래스 Book으로 생성된 객체의 멤버 함수를 선언합니다.

 

- 위 코드에서 'b'는 'Novel' 클래스의 인스턴스를 가리키고 있습니다. 'Novel' 클래스는 'Book' 클래스를 상속하고 있으며

'print' 메소드를 오버라이딩하고 있습니다. 따라서 'b.print()'를 호출하면 'Novel' 클래스에서 오버라이딩된 'print' 메소드가 호출됩니다. 

 

- 위 코드에서 'c'는 'SF' 클래스의 인스턴스를 가리키고 있습니다. 'SF' 클래스는 'Book' 클래스를 상속하고 있으며

'print' 메소드를 오버라이딩하고 있습니다. 따라서 'b.print()'를 호출하면 'Novel' 클래스에서 오버라이딩된 'print' 메소드가 호출됩니다. 

 

부모 클래스로 객체를 선언하였으나 실행시점에 동적 바인딩되어 자식클래스의 멤버함수가 호출되는 것을 볼 수 있습니다. 

 

📌 6. 형변환(캐스팅)

- 하나의 데이터 타입을 다른 타입으로 바꾸는 것을 타입 변환 혹은 형변환(캐스팅) 이라고 합니다.

 

자바의 데이터형을 알아보면 크게 두 가지로 나뉘게 된다.

1. 기본형 (primitive type) - Boolean Type(boolean) - Numeric Type(short, int, long, float, double, char)

2. 참조형 (reference type) - Class Type - Interface Type - Array Type - Enum Type - 그 외 다른 것들

- 서로 타입 간의 형변환(casting) 이 가능하다는 뜻입니다. 

 

📌 Upcasting (업캐스팅)

- 업캐스팅은 자식 클래스가 부모 클래스 타입으로 캐스팅 되는 것입니다. 

- 업캐스팅은 캐스팅 연산자 괄호를 생략할 수 있습니다. 

- 부모 클래스로 캐스팅 된다는 것은 멤버의 갯수 감소를 의미한다.

- 업캐스팅을 하고 메소드를 실행할 때, 만일 자식 클래스에서 오버라이딩한 메서드가 있을 경우, 부모 클래스의 메서드가 아닌

오버라이딩된 메서드가 실행되게 된다. 

- 업캐스팅을 하면 멤버 갯수가 제한되어 자식 클래스에만 있는 멤버는 사용할 수 없게 된다. 

- 업캐스팅 했지만 오버라이딩된 메서드는 자식 클래스의 메서드로 실행이 된다. 

- 상속 관계에서 상속 받은 서브 클래스가 몇 개이든 하나의 인스턴스로 묶어서 관리할 수 있기 때문이다. 

 

📌 Downcasting (다운캐스팅)

- 다운캐스팅은 부모 클래스가 자식 클래스 타입으로 캐스팅 되는 것입니다. 

- 다운캐스팅은 캐스팅 연산자 괄호를 생략할 수 없습니다.

- 다운캐스팅의 목적은 업캐스팅한 객체를 다시 지식 클래스 타입의 객체로 되돌리는데 목적을 둡니다. 

- 부모 클래스로 업캐스팅된 자식 클래스를 복구하여 본인의 필드와 기능을 회복하기 위해 있는 것입니다.

(원래 있던 기능을 회복하기 위해 다운캐스팅을 하는 것입니다.)

 

📌 7. Instanceof 연산자

- 다운캐스팅은 부모 클래스가 자식 클래스 타입으로 캐스팅 되는 것입니다. 이 연산자는 어느 객체 변수가 어느 클래스 타입인지 판별해

true/false를 반환해줍니다. 객체에 대한 클래스(참조형) 타입에만 사용할 수 있다는 점입니다. 

 

📌 8. 다형성(Ploymorphism) 이용하여 Class 구현하기 예제

 

 

- 제품(Product)을 구입하는 Buyer 클래스를 정의하고 Tv와 Computer 클래스를 상속하여 각각의 제품을 나타냅니다. 

Buyer 클래스에는 buy 메서드가 있어서 제품을 구매하고 구매한 제품의 가격만큼 돈을 차감하고 보너스 점수를 적립합니다. 

 

<< 문제 추가 내용 >>

1. Buyer 클래스 내에 멤버 필드를 추가합니다. 이 필드는 길이가 10인 배열로서 구매한 가전 제품 정보를 저장합니다. 

2. Summary 메서드를 추가합니다. 이 메서드는 구매한 가전 제품 정보를 출력하고 총 금액과 제품명을 출력합니다. 

 

 

위의 코드는 제품 (Product)을 나타내는 클래스입니다. 해당 클래스는 제품의 가격과 보너스 포인트를 저장합니다. 생성자를 통해 가격을 받아 들이고 보너스 포인트는 가격의 10%를 적립합니다. 

 

Product(int price) {
	this.price = price;
    bonusPoint = (int)(price/10.0);
    }

 

'Product' 클래스의 생성자를 정의하고 있습니다. 이 생성자는 가격을 매개변수로 받아서 제품의 가격과 보너스 포인트를 설정합니다. 

this.price = price;  생성자에게 전달된 'price' 매개변수를, 'Product' 클래스의 'price' 필드에 할당하는 역할을 합니다.

 

bonusPoint = (int)(price/10.0); 는 주어진 가격을 10으로 나눈 후에 이를 정수로 변환하여 보너스 포인트로 설정하는 코드입니다. 보너스 포인트는 제품 가격의 10%로 설정되며, 소수 부분은 버려집니다. 

 

class Tv extends Product {
	Tv() {
    	super(100);
    }
    
    @Override
    public String toString() {
    	return "Tv";
    }

 

'Product'  클래스를 상속받는 'Tv' 클래스를 정의하고 있습니다.  'Tv()' 는 'super(100)'을 호출하여 부모 클래스인  'Product' 클래스의 생성자를 호출하고 있습니다. 이는 Tv의 가격을 100으로 설정하고 부모 클래스인 'Product'의 생성자를 호출하여 가격에 따른 보너스 포인트를 계산하기 위함입니다. 

 

@Override 어노테이션을 사용하여 부모 클래스인 'Product' 클래스의 'toString()' 메서드를 오버라이드하고 있습니다. 따라서 'Tv' 클래스의 인스턴스를 출력할 때 "Tv" 라는 문자열을 반환하도록 설정되었습니다.

 

class Computer extends Product {
	Computer() {
    	super(200);
    }
    
    @Override
    public String toString() {
    	return "Computer";
    }

 

'Product' 클래스를 상속받는 'Computer' 클래스를 정의하고 있습니다. 'Computer()'는 super(200)'을 호출하여 부모클래스인 'Product' 클래스의 생성자를 호출하고 있습니다. 이는 Computer 가격을 200으로 설정하고 부모 클래스인 'Product' 생성자를 호출하여 가격에 따른 보너스 포인트를 계산하기 위함입니다. 

 

@Override 어노테이션을 사용하여 부모 클래스인 'Product' 클래스의 'toString()' 메서드를 오버라이드하고 있습니다. 따라서 'Tv' 클래스의 인스턴스를 출력할 때 "Tv" 라는 문자열을 반환하도록 설정되었습니다.

 

 

'Buyer' 클래스의 'buy' 메서드는 'Product' 객체를 매개변수로 받아서 해당 제품을 구매하는 기능을 수행합니다. 

구매를 위해선 구매자의 잔액 ('money')이 제품의 가격('product.price') 보다 충분한 지 확인합니다. 잔액이 부족하면 살 수 없다는 메시지를 출력하고 

메서드를 종료합니다. 충분한 잔액이 잇을 경우에는 제품의 가격만큼 잔액을 차감하고 보너스 포인트를 증가시킵니다. 

 

'void buy(Product product)' 는 "제품을 구매하는" 메서드를 의미합니다. 이 때 Product는 참조변수입니다.  

* 참조변수(Reference Variable) : 객체를 가리키는 변수, Java에서 모든 객체는 힙(heap) 메모리에 생성되며 이러한 객체는 객체 변수나 배열 요소에

저장됩니다. 객체가 생성된 힙 메모리의 주소(참조) 가 저장됩니다. 이 주소를 사용하여 실제 객체에 접근하고 사용할 수 있습니다. 

 

 

'void buy(Product product)' 는 "제품을 구매하는" 메서드를 의미합니다. 이 때 Product는 참조변수입니다.  

* 참조변수(Reference Variable) : 객체를 가리키는 변수, Java에서 모든 객체는 힙(heap) 메모리에 생성되며 이러한 객체는 객체 변수나 배열 요소에

저장됩니다. 객체가 생성된 힙 메모리의 주소(참조) 가 저장됩니다. 이 주소를 사용하여 실제 객체에 접근하고 사용할 수 있습니다. 

 

'public static void main(String[] args) {  '  : 프로그램의 시작점. 메서드.  * public : 'main' 메서드가 어디서든지 접근 가능함을 나타냅니다.

* static : 'main' 메서드는 인스턴스화되지 않은 클래스 수준에서 실행됩니다.

* void : 'main' 메서드가 반환하는 값이 없음.

*String[] args : 명령행 인수 (arguments)를 받는 매개변수. 프로그램을 실행할 때 커맨드 라인에서 전달된 인수들이 배열로 전달.

 

Buyer b = new Buyer();  : Buyer 클래스의 객체를 생성하여 변수 'b'에 할당하는 것.

Buyer 는 클래스 이름, b는 객체를 가르키는 참조변수. new Buyer() 는 클래스의 새로운 인스턴스를 생성.

 

b.buy(new Tv()); : Buyer 클래스의 buy 메서드를 호출하는 것으로 'Product' 객체를 매개변수로 받습니다.

'Tv' 클래스의 객체를 생성하여 'buy' 메서드에 전달하고 있습니다.

 

b.buy(new Tv()); : Buyer 클래스의 buy 메서드를 호출하는 것으로 'Product' 객체를 매개변수로 받습니다.

'Tv' 클래스의 객체를 생성하여 'buy' 메서드에 전달하고 있습니다.

 

System.out.println("현재 남은 돈은" + b.money + "만원입니다."); 

System.out.println("현재 보너스 점수는" + b.bonusPoint + "점입니다.");

: Buyer 객체인 b의 현재상태를 출력하는 것입니다. Buyer 클래스의 멤버변수인 'money'와 'bonusPoint'를 사용하여 현재 남은 돈과 

현재 보너스 점수를 출력합니다.