JAVA

다형성과 instanceof

쿠키는고양이 2022. 8. 28. 15:49

 

오늘은 다형성과 instanceof 예약어에 대해 정리한다.

 

지난 포스팅에서 정리한 클래스의 4가지 특성은 다음과 같다.

1. 캡슐화(정보은닉)

2. 상속

3. 다형성

4. 추상화

 

다형성이란 다양한 형태를 가지는 성질을 의미한다.

따라서 클래스에서 말하는 다형성은 인스턴스가 다른 클래스의 형태로 변할 수 있다는 말이다.

즉, 형태가 바뀌기 때문에 '형변환' 에 대해서도 언급하고자 한다.

 

1. 다형성 개념

우선 클래스란 변수 + 메소드를 하나의 객체로 묶어주는 하나의 사용자 자료형 이라는 점을 이해해야 한다.

즉, 클래스란 int, long, float 과 같이 자료형으로써 사용할 수 있다. 이 점을 꼭 기억하자.

 

다형성이란 부모 클래스의 변수로 자손 클래스의 인스턴스를 저장할 수 있다는 개념이다.

즉, 하나의 변수에 서로 다른 인스턴스를 저장할 수 있다.

따라서 다형성의 특징을 살리기 위해서는 꼭 상속관계에 있는 부모 클래스와 자식 클래스가 있어야 한다.

상속관계에 있다 하더라도 다형성을 만족하기 위해서는 아래의 조건을 만족해야한다.

클래스 A를 상속받은 B 클래스가 있다고 한다면, 부모의 클래스 변수에 자식 클래스 타입의 대입은 가능하다.

 

반대로 자식 클래스의 변수에 부모 클래스의 타입은 대입이 불가능하다.

 

여기서 주목해야할 것은 우측에 있는 타입이다.

클래스를 만드는 것은 하나의 자료형을 만드는 것이기에 상속을 받은 자식 클래스여도 부모 클래스에 대입되기 위해서는 반드시 자료형이 바뀌어야 한다. 즉, 형변환이 필요하게 된다.

 

상속관계를 잘 생각해보면 부모는 여러 자식 클래스를 가질 수 있지만, 자식은 여러 부모를 가질 수 없다. 그렇기에

개념상으론 부모 클래스는 항상 자식 클래스보다 큰 개념이다. (물론 메모리상에선 자식이 부모보다 크거나 같다.)

따라서 작은 것을 큰 것에 넣는 것은 별다른 문제가 발생하지 않는다. 따라서 컴파일러는 문제가 발생하지 않으니

자동으로 형변환을 진행하여 대입을 할 수 있다. 이를 자동 타입 변환(Promotion) 이라 한다.

 

하지만 case 2. 와 같이 큰 것을 작은 것에 넣게 되면 에러가 발생한다. 따라서 작은 것에 들어갈 수 있도록 형태를 바꿔야 하는데 이를 '강제 형변환(Casting)' 이라 한다.

 

즉, 다형성이랑 "자동 타입 변환" 과 "강제 형변환" 을 통해 클래스 본래의 형태를 바꾸어 사용하는 것을 말한다.

 

프로그램의 수많은 객체들은 서로 연결되고 각각의 역활을 맡는데 이 객체들이 다른 객체로 교체되며 더 뛰어난

성능을 가진 코드가 될 수 있다. 마치 자동차의 부품을 더 좋은 부품으로 교체하는 것과 비슷하다.

 

그리고 이런 다형성은 상속, 오버라이딩, 형변환을 통해 구현할 수 있다.

 

2. 다형성 사용하기

다형성을 사용하기 전에 왜 다형성이 필요한가를 먼저 살펴보자.

아래의 코드는 국가와 도시의 정보를 입력하는 예제이다.

 

<부모 클래스>

package com;

import java.util.Scanner;

// 부모 클래스
public class Nation {
	protected String name;	// 이름
	protected int people;	// 인구
	
	Scanner scan = new Scanner(System.in);

	public void output() {
		System.out.println("** 국가 정보 출력 **");
		System.out.println("국가 : " + name);
		System.out.println("인구 : " + people);
	}
	
	public void input() {
		System.out.println("** 국가 정보 입력 **");
		
		System.out.print("국가 : ");
		name = scan.nextLine();
		System.out.print("인구 : ");
		people = scan.nextInt();
		scan.nextLine();
	}
}
 

<자식 클래스>

package com;

// 자식 클래스
public class City extends Nation{
	public String cityName;
	public String famousArea;
	public String famousFood;
	
	@Override
	public void output() {
		// TODO Auto-generated method stub
		super.output();
		System.out.println("** 도시 정보 출력 **");
		System.out.println("도시 : " + cityName);
		System.out.println("관광지 : " + famousArea);
		System.out.println("대표음식 : " + famousFood);
	}
	
	@Override
	public void input() {
		// TODO Auto-generated method stub
		super.input();
		System.out.println("** 도시 정보 입력 **");
		System.out.print("도시 : ");
		cityName = scan.nextLine();
		System.out.print("관광지 : ");
		famousArea = scan.nextLine();
		System.out.print("대표음식 : ");
		famousFood = scan.nextLine();
	}
}
 

<main 클래스>

package com;

import java.util.Scanner;

public class PolyManager {
	static Scanner scan = new Scanner(System.in);
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Nation[] nArr = new Nation[5];
		City[] cArr = new City[5];
		
		int nCnt = 0;
		int cCnt = 0;
		
		while(true) {
		
			switch(printMenu()) {
			case 1:
				Nation n = new Nation();
				n.input();
				nArr[nCnt] = n;
				nCnt++;
				break;
				
			case 2:
				City c = new City();
				c.input();
				cArr[cCnt] = c;
				cCnt++;
				break;
				
			case 3:
				for(int i = 0; i < nCnt; i++) {
					nArr[i].output();
				}
				break;
			case 4:
				for(int i = 0; i < cCnt; i++) {
					cArr[i].output();
				}
				break;
			}
		}
	}

	public static int printMenu() {
		System.out.println("=============================");
		System.out.println("1. 국가 정보 입력, 2. 도시 정보 입력");
		System.out.println("3. 국가 정보 출력, 4. 도시 정보 출력");
		System.out.println("=============================");
		System.out.print("입력>> ");
		int select = scan.nextInt();
		return select;
	}
}
 

 

위 코드에서 주목해야하는 점은 main 클래스 쪽에서 각 클래스의 배열이다.

서로 다른 배열을 잡은 이유는 당연히 상속을 하였어도 다른 형태의 객체를 만들기 때문이다.

즉, Nation 클래스의 인스턴스는 Nation형, City 클래스의 인스턴스는 City 형이다.

만약, Nation을 상속받은 자식클래스가 더욱 더 많아진다면 계속하여 각 인스턴스를 담을 배열을 만들어야 한다.

그렇다면 배열을 각각 만들지 말고 하나의 배열에 넣어 통합적으로 관리할 순 없을까?

 

이때 다형성을 사용하면 간단하게 문제를 해결할 수 있다.

아래의 코드는 main 클래스 부분을 변경한 것이다. (다른 클래스는 변경되지 않았다.)

package com;

import java.util.Scanner;

public class PolyManager {
	static Scanner scan = new Scanner(System.in);
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		Nation[] nArr = new Nation[10];
		//Nation[] nArr = new Nation[10];
		//City[] cArr = new City[5];
		
		int nCnt = 0;
		//int cCnt = 0;
		
		while(true) {
		
			switch(printMenu()) {
			case 1:
				Nation n = new Nation();
				n.input();
				nArr[nCnt] = n;
				nCnt++;
				break;
				
			case 2:
				City c = new City();
				c.input();
				nArr[nCnt] = c;
				nCnt++;
				break;
				
			case 3:
				for(int i = 0; i < nCnt; i++) {
					if(nArr[i] instanceof Nation) {
						Nation nation = (Nation)nArr[i];
						nation.output();
					}	
				}
				break;
				
			case 4:
				for(int i = 0; i < nCnt; i++) {
					if(nArr[i] instanceof City) {
						City city = (City) nArr[i];
						city.output();
					}
				}
				break;
			}
		}
	}

	public static int printMenu() {
		System.out.println("=============================");
		System.out.println("1. 국가 정보 입력, 2. 도시 정보 입력");
		System.out.println("3. 국가 정보 출력, 4. 도시 정보 출력");
		System.out.println("=============================");
		System.out.print("입력>> ");
		int select = scan.nextInt();
		return select;
	}
	
}
 

주목할 부분은 case 3 과 case 4 이다. (보기 좋게 아래에 같은 내용을 적어 놨다.)

                       case 3:
				for(int i = 0; i < nCnt; i++) {
					if(nArr[i] instanceof Nation) {
						Nation nation = (Nation)nArr[i];
						nation.output();
					}	
				}
				break;
				
//			case 3:
//				for(int i = 0; i < nCnt; i++) {
//					nArr[i].output();
//				}
//				break;
			

			case 4:
				for(int i = 0; i < nCnt; i++) {
					if(nArr[i] instanceof City) {
						City city = (City) nArr[i];
						city.output();
					}
				}
				break;
				
//			case 4:
//				for(int i = 0; i < cCnt; i++) {
//					cArr[i].output();
//				}
//				break;
 

주석된 부분은 다형성을 사용하기 전의 코드이다. 변경된 코드와 비교를 해보자면 다음과 같이 변경됐다.

1. for문 안에 if문 instanceof 가 생겼다.

2. case 4 에서 cArr[i] -> nArr[i]  cCnt -> nCnt 로 바뀌었다.

 

우선 2번의 경우를 먼저 살펴본다면 각 클래스별 배열이 아닌 하나의 배열로 사용되는 것을 알 수 있다.

또한 배열 인덱스를 카운팅하기 위한 변수도 nCnt 한 개만 사용된다.

이렇게 하나의 배열로 두 클래스의 값을 넣었어도 실행결과는 동일할까?

우측의 출력결과를 보면 3번 국가정보 출력에서는 일본의 도시정보까지 출력이 되고 있다.

그리고 4번에서는 기존 결과와 마찬가지로 도시의 정보가 나오고 있다.

 

3번의 경우 신기한 점은 각 클래스에서는 출력하는 내용이 다르며 배열은 Nation 형으로 정의가 되었어도

Nation을 상속받은 City 클래스의 정보까지 함께 출력이 되었다는 것이다.

즉, City 인스턴스가 Nation 형으로 변환되었지만 오버라이딩된 자신의 메소드로 출력된 것을 볼 수 있다.

 

4번의 경우는 Nation 형의 배열을 출력하는데 이상하게도 같은 배열에 있던 한국의 정보는 출력되지 않았다.

이는 instanceof 라는 예약어를 사용하였기 때문이다.

 

3. instanceof

instanceof란 어떤 클래스의 인스턴스인지 확인하기 위한 키워드이다.

즉, 인스턴스와 클래스의 형을 비교하여 같다면 true, 틀리면 false 가 된다.

                                case 2:
				City c = new City();
				c.input();
				nArr[nCnt] = c;
				nCnt++;
				break;

                                case 4:
				for(int i = 0; i < nCnt; i++) {
					if(nArr[i] instanceof City) {
						City city = (City) nArr[i];
						city.output();
					}
				}
				break;
 

위 코드를 다시 보면 nArr[i] 배열은 Nation 형이다. 하지만 입력할때(case 2) City형 인스턴스를 만들어 배열에

넣은 것을 확인할 수 있다. 즉 nArr[] 에는 City형의 인스턴스가 들어간 것(자동 타입 변환)이다.

이 때 nArr[i] 과 City 의 형이 같다면 if문은 true가 되므로 안의 내용을 실행하게 된다.

if문 안에서는 City형 인스턴스를 생성하고 nArr[i]의 인스턴스를 강제로 City 형으로 형변환을 하고 있다.

이는 City형이 nArr 에 들어가기 위해 Nation 형으로 변환된 것을 다시 City 형으로 변환해 준 것이다.

이 때 Nation 형은 부모 클래스이므로 자식인 City에 들어가기 위해 강제로 형변환 한 것을 볼 수 있다.

 

이번 포스팅에서는 상속과 형변환을 이용하여 다형성을 구현했다. 다음 포스팅에서는 오버라이딩을 활용하여

매개변수 다형성에 대해 정리한다.