본문 바로가기

JAVA/JAVA 이론

[JAVA 이론] 다형성(polymorphism)

[JAVA 심화]

1. 상속(Inheritance)

2. 캡슐화(encapsulation)

3. 다형성(Polymorphism)

4. 추상화(Abstraction)


개요

자바 객체지향 프로그래밍의 네 가지 요소 중 가장 핵심적인 부분인 다형성에 대해서 공부해 보도록 하겠습니다.


학습목표

  • 자바 객체지향 프로그래밍에서 다형성이 가지는 의미와 장점을 이해할 수 있다.
  • 참조변수 타입 변환에 대한 내용을 이해하고, 업캐스팅과 다운캐스팅의 차이를 설명할 수 있다.
  • instanceof 연산자를 활용하는 방법을 이해하고 설명할 수 있다.
  • 코딩 예제를 실제로 입력해 보면서 다형성이 실제로 어떻게 활용되는지 이해할 수 있다.

다형성

다형성(polymorphism)이란 "여러 개"를 의미하는 poly와 어떤 "형태"를 의미하는 morphism의 결합어로 하나의 객체가 여러 가지 형태를 가질 수 있는 성질을 의미합니다.

자바에서의 다형성은 한 타입의 참조 변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다.

좀 더 구체적으로 얘기하면, 상위 클래스 타입의 참조변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용된 것이라 할 수 있습니다.

다음의 코드 예제를 통해 한번 살펴보겠습니다. 

class Person {
	public void personInfo() {
    	System.out.println("I'm an ordinary man");
    }
}

class Student extends Person {
	public void personInfo(){
    	System.out.println("i'm student");
    }
}

class Programmer extends Person {
	public void personInfo(){
    	System.out.println("i'm programmer");
    }
}

public class Test{
	public static void main(String[] args){
    	Person p = new Person();
        Student s = new Student();
        Person pg = new Programmer();
        
        p.personInfo();
        s.personInfo();
        pg.personInfo();
    }
}

// 출력 결과
i'm an ordinary man
i'm student
i'm programmer

위의 예제 코드를 보면 참조변수 person과 student 모두 각각 타입이 일치하지만 programmer는 타입이 다른 것을 확인할 수 있습니다. 원래라면 타입을 일치시키기 위해 Programmer를 참조변수 타입으로 지정해주어야 하지만, 그러지 않고 상위 클래스 Person을 타입으로 선언해 주었습니다.

이 경우, 상위 클래스를 참조변수 타입으로 지정했기 때문에 자연스럽게 참조변수가 사용할 수 있는 멤버의 개수는 상위 클래스의 멤버의 수가 됩니다.

이것은 앞서 상속에서 설명하였던 [JAVA 심화] 상속 글에서 설명하였던 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조하는 것이자 다형성의 핵심적인 부분이라 할 수 있습니다.

다음으로, 다형성의 핵심 중의 하나는 상위 클래스의 타입으로 하위 클래스 타입의 객체를 참조하는 것은 가능하지만, 그 반대는 성립되지 않는다는 것입니다.

 

예제 코드를 통해 확인해 보도록 하겠습니다.

public class Test {
	public static void main(String[] args){
    	Person p = new Person(); // 객체 타입과 참조 변수 타입 일치
        Student s = new Student();
        Person pg = new Programmer(); // 객체 타입과 참조변수 타입의 불일치 -> 가능
      //Programmer pg1 = new Person(); -> 하위 클래스 타입으로 상위 클래스 객체 참조 -> 불가능
    
    	p.personInfo();
        s.personInfo();
        pg.personInfo();
    }
}

위의 예제 코드를 보면 person 타입으로 하위 클래스인 Programmer를 참조하는 것은 가능하지만, 그 반대로 하위 클래스 Programmer 타입으로 상위 객체 Person을 참조하는 것은 불가능합니다.

그 이유는 실제 객체인 Person의 멤버 개수보다 참조변수 pg1이 사용할 수 있는 멤버 개수가 더 많기 때문입니다.

이는 앞서 배웠던 메서드 오버라이딩과 메서드 오버로딩 또한 다형성의 한 예시라는 것을 말해줍니다.


참조변수의 타입 변환

참조 변수의 타입 변환은 사용할 수 있는 멤버의 개수를 조절하는 것을 의미하는데, 이는 자바의 다형성을 이해하기 위해서 꼭 필요한 개념입니다.

타입 변환을 위해서는 다음의 세 가지 조건을 충족하여야 합니다.

  1. 서로 상속관계에 있는 상속 클래스 - 하위 클래스 사이에만 타입 변환이 가능합니다.
  2. 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형변환 연산자(괄호)를 생략할 수 있습니다.
  3. 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형변환 연산자(괄호)를 반드시 명시해야 합니다.
    • 또한, 다운 캐스팅은 업 캐스팅이 되어 있는 참조변수에 한해서만 가능합니다.

예제 코드를 보며 이해를 해봅시다.

public class Test {
	public static void main(String[] args){
    	Programmer p = new Programmer();
        Person pg = (Person) p; // 상위 클래스 Person 타입으로 변환(생략 가능)
        Programmer p2 = (Programmer) pg;	// 하위 클래스 Programmer타입으로 변환(생략 불가능)
        Dancer d = (Dancer) p;	// 상속관계가 아니므로 타입 변환 불가 -> 에러 발생 
    }
}

class Person {
	String name;
    String job;
    int age;
    
    void run() {
    	System.out.println("run");
    }
    
    void eat() {
    	System.out.println("eat");
    }
}

class Programmer extends Person{
	void coding() {
    	System.out.println("coding");
    }
}

class Dancer extends Person{
	void dancing() {
    	System.out.println("dancing");
    }
}

위 예시를 보면 먼저 Person 클래스가 있고 Person 클래스를 상속받은 Programmer와 Dancer 클래스가 있습니다.

위 예시 코드와 같이 상속관계에 있는 클래스 간에는 상호 타입변환이 수행될 수 있습니다. 다만 하위 클래스를 상위 클래스 타입으로 변환하는 경우 타입 변환 연산자(괄호)를 생략할 수 있는 반면, 그 반대의 경우는, 업 캐스팅이 되어있다는 전제 하에 다운 캐스팅을 할 수 있으며, 타입 변환 연산자를 생략할 수 없다는 점에서만 차이가 있다고 할 수 있습니다.

 

그리고, Programmer와 Dancer 클래스는 상속관계가 아니므로 타입 변환이 불가하여 에러가 발생하는 것을 확인할 수 있습니다.


instanceof 연산자

instanceof 연산자는 앞서 배웠던 참조변수의 타입 변환, 즉 캐스팅이 가능한 지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법 요소입니다.

캐스팅 가능 여부를 판단하기 위해서는 두 가지, 즉 객체를 어떤 생성자로 만들었는가와 클래스 사이에 상속 관계가 존재하는가를 판단하여야 합니다.

참조_변수 instanceof 타입

참조_변수 instanceof 타입을 입력했을 때 리턴 값이 true가 나오면 참조 변수가 검사한 타입으로 타입 변환이 가능하며, 반대로 false가 나오는 경우 타입 변환이 불가능합니다.

코드 예제를 통해 자세히 알아봅시다.

public class InstanceOfExample {
 	public static void main(String[] args) {
 		Person nom = new Person();
        	System.out.println("nom instanceof Object"); // true
        	System.out.println("nom instanceof Person"); // true
        	System.out.println("nom instanceof Programmer"); //false
        
        	Person programmerNom = new Programmer();
        	System.out.println("programmerNom instanceof Object"); // true
        	System.out.println("programmerNom instanceof Person"); // true
        	System.out.println("programmerNom instanceof Programmer"); // true
        	System.out.println("programmerNom instanceof Dancer"); // false
 	}
}

class Person{};
class Programmer extends Person{};
class Dancer extends Person{};

 

위 예제를 보면 객체의 타입을 instanceof 연산자를 통해 확인하여 형변환 여부를 확인할 수 있습니다.

이를 통해 에러를 최소화하는 유용한 수단이 될 수 있습니다.


다형성의 활용 예제

이제 자바 객체지향 프로그래밍에서 다형성이 실제로 어떻게 활용될 수 있는 지를 실제 코딩 예제를 통해서 좀 더 살펴보도록 하겠습니다.

 

class Coffee {
	int price;
    
    public Coffee(int price) {
    	this.price = price;
    }
}

class Americano extends Coffee{};
class CaffeLatte extends Coffee{};

class Customer {
	int money = 50000;
}

위의 코드 예제를 보면 총 4개의 클래스를 확인할 수 있습니다. 커피의 가격 정보를 가지고 있는 Coffee 클래스가 있고, 이를 상속받는 Americano 클래스와 CaffeLatte 클래스가 있습니다. 마지막으로 Customer 클래스는 50,000의 money를 가지고 있다고 가정해 봅시다.

 

여기서 Americano와 CaffeLatte 한잔을 구입한다고 가정하였을 때 이에 대한 메서드를 추가해 보도록 하겠습니다

class Coffee {
	int price;
    
    public Coffee(int price) {
    	this.price = price;
    }
}

class Americano extends Coffee{};
class CaffeLatte extends Coffee{};

class Customer {
	int money = 50000;
    
    void buyCoffee(Americano americano){
    	money -= americano.price;
    }
    void buyCoffee(CaffeLatte caffeLatte){
    	money -= caffeLatte.price;
    }
}

사야 하는 커피를 구분하기 위해서 매개변수로 각각 Americano 타입과 CaffeLatte 타입의 객체를 전달해 주었습니다.

하지만, 만약에 손님이 구입하는 커피의 종류가 많다면 매번 새로운 타입을 매개변수로 전달해 주는 buyCoffee 메서드를 계속 추가해주어야 할 것입니다.

이런 경우 앞서 공부하였던 객체의 다형성을 활용하여 아래와 같이 문제를 해결할 수 있습니다.

 

class Coffee {
	int price;
    
    public Coffee(int price) {
    	this.price = price;
    }
}

class Americano extends Coffee{};
class CaffeLatte extends Coffee{};

class Customer {
	int money = 50000;
    
    void buyCoffee(Coffee coffee){
    	money -= Coffee.price;
    }
}

위 예제 코드를 보면 상위 클래스인 coffee의 타입을 매개변수로 전달받으면, 그 하위클래스 타입의 참조변수면 어느 것이나 매개 변수로 전달될 수 있고 이에 따라 매번 다른 타입의 참조변수를 매개변수로 전달해주어야 하는 번거로움을 훨씬 줄일 수 있습니다.

 

이제 전체적인 코드를 보도록 하겠습니다

 

 

public clas PoloymorphismEx {
 pulbic static void main(String[] args){
 	Customer c = new Customer();
    c.buyCoffee(new Americano());
    c.buyCoffee(new CaffeLatte());
    
    System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
 	
 }
}
class Coffee {
	int price;
	public Coffee(int price){
    	this.price = price;
    }
}


class Americano extends Coffee{
	public Americano(){
    	super(4000); // 상위 클래스 cOFFEE의 생성자를 호출
    }

	public String toString() {return "아메리카노";} //Object 클래스 toString() 메서드 오버라이딩
}

class CaffeLatte extends Coffee{
	public CaffeLatte(){
    	super(5000);
    }
 	public String toString() {return "카페라떼";}
}

class Customer {
	int money = 50000;
    
    void buyCoffee(Coffee coffee){
    	if(money < coffee.price) {
        	System.out.println("잔액이 부족합니다.");
            return;
        }
        money -= coffee.price;
        System.out.println(coffee + "를 구입했습니다.");
    }
}

// 출력 결과
아메리카노를 구입했습니다.
카페라떼를 구입했습니다.
현재 잔액은 41000원 입니다.

위의 코드 예제를 보면 객체지향 설계의 다형성을 활용하여 buyCoffee() 메서드의 매개변수로 Coffee 타입을 전달해 주었습니다.

위의 방법으로 많은 중복되는 코드를 줄이고 보다 편리하게 코드를 작성하는 것이 가능해집니다.


이번 글을 통하여 다형성에 대해 공부해 보았습니다.

다음 글은 자바 객체지향 프로그래밍의 네 가지 주요 특성 중 마지막 특성인 추상화에 대해 공부해 보도록 하겠습니다.

=> [JAVA 심화] 추상화(Abstraction)