[혼공자] 07-2. 타입 변환과 다형성

Benjamin·2023년 3월 11일
0

혼공자

목록 보기
22/27

07-2. 타입 변환과 다형성

앞에서 배운 재정의와 이번 절의 타입변환을 이용하면 객체 지향 프로그래밍의 중요한 특징인 다형성을 구현할 수 있다.

다형성

사용 방법은 동일하지만 다양한 객체를 이용해서 다양한 실행 결과가 나오도록 하는 성질
ex) 자동차가 타이어 사용하는 방법은 동일하나, 어떤 타이어를 사용(장착)하느냐에 따라 주행 성능이 달라질 수 있다.

다형성을 구현하려면 메소드 재정의와 타입 변환이 필요하다.

메소드 재정의 + 타입변환 => 다형성

자동 타입 변환 (promotion)

타입 변환 = 타입을 다른 타입으로 변환하는 행위

기본 타입의 타입 변환과 마찬가지로 클래스도 타입 변환이 있다.
클래스의 변환은 상속 관계에 있는 클래스 사이에서 발생한다.
자식은 부모 타입으로 자동 타입 변환이 가능하다.

자동 타입 변환은 프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말한다.

자식은 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급될 수 있다는 것이다.
예를 들어, 고양이가 동물의 특징과 기능을 상속받았으면, '고양이는 동물이다.'가 성립한다.

다음의 예를 보자.

Cat클래스로 부터 Cat 객체를 생성하고, 이것을 Animal변수에 대입하면 자동 타입 변환이 일어난다.

Cat cat = new Cat();
Animal animal = cat;

//Animal animal = new Cat(); 도 가능 

위 코드로 생성되는 메모리 상태는 다음과 같다.
cat과 animal변수는 타입만 다를 뿐, 동일한 Cat 객체를 참조한다.

cat == animal //true

바로 위의 부모가 아니더라도 상속 계층에서 상위타입이면 자동 타입 변환이 일어날 수 있다.

다음 그림을 보고 이해하자.

부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근 가능하다.
비록 변수는 자식 객체를 참조하지만, 변수로 접근 가능한 멤버는 부모 클래스 멤버로만 한정된다.
그러나 예외가 있다.
메소드가 자식 클래스에서 재정의되었다면 자식 클래스의 메소드가 대신 호출된다.
-> 다형성과 관련있기때문에 매우 중요한 성질!

Child 객체는 method3() 메소드를 가지고있지만, Parent 타입으로 변환된 이후에는 method3()을 호출할 수 없다.
그러나 method2()는 자식과 부모 모두에게 있다.
이렇게 재정의된 메소드는 타입 변환 이후에도 자식 메소드가 호출된다.

필드의 다형성

왜 자동 타입 변환이 일어날까?
다형성을 구현하기 위해서이다.

필드의 타입을 부모 타입으로 선언하면 다양한 자식 객체들이 저장될 수 있기때문에 필드 사용 결과가 달라질 수 있다 = 필드의 다형성

예를 들어, 자동차를 구성하는 부품은 언제든지 교체될 수 있다.
객체지향 프로그래밍에서도 마찬가지로 수많은 객체들이 서로 연결되고 각자의 역할을 하게 되는데, 이 객체들은 다른 객체로 교체될 수 있어야 한다.

자동차 클래스에 포함된 타이어 클래스를 생각해보자.
새로 교체되는 타이어 객체는 기존 타이어와 사용 방법은 동일하지만 실행 결과는 더 우수하게 나와야 할 것이다.
이것을 프로그램으로 구현하기 위해 상속과 재정의, 타입 변환을 이용한다.

부모 클래스를 상속하는 자식 클래스는 부모가 가지고있는 필드와 메소드를 가지고 있으니 사용 방법이 동일할것이다.
자식 클래스는 부모 메소드를 재정의해서 메소드의 실행 내용을 변경함으로써 더 우수한 실행결과가 나오게 할 수도 있다.
그리고 자식 타입을 부모 타입으로 변환할 수 있다.
이 세가지가 다형성을 구현할 수 있는 기술적 조건이 된다.

필드의 다형성을 코드로 보자.

class Car {
	//필드
    Tire frontLeftTire = new Tire();
    Tire frontRightTire = new Tire();
    Tire backLeftTire = new Tire();
    Tire backRightTire = new Tire();
    
    //메소드
    void run(){...}
}

Car클래스는 4개의 Tire 필드를 갖고있다.
Car 클래스로부터 Car 객체를 생성하면 4개의 Tire 필드에 각각 하나씩 Tire 객체가 들어간다.
그런데 만약 frontRightTire,backLeftTire를 HankookTire, KumhoTire로 교체할 이유가 생겼다.
이런 경우는 다음과 같은 코드를 사용해 교체할 수 있다.

Car myCar = new Car();

myCar.frontRightTire = new HankookTire();
myCar.backLeftTire = new KumhoTire();
myCar.run();

Tire 클래스 타입인 frontRightTire,backLeftTire는 원래 Tire 객체가 저장되어야하지만, Tire의 자식 객체가 저장되어도 문제없다.
왜냐하면 자식 타입은 부모 타입으로 자동 타입 변환되기 때문이다.

자식 객체가 저장되어도 Car 객체는 Tire 클래스에 선언된 필드와 메소드만 사용하므로 전혀 문제가 되지않는다.

Car 객체에 run() 메소드가 있고, run()메소드는 각 Tire 객체의 roll()메소드를 다음과 같이 호출한다고 가정해보자.

void run() {
	frontLeftTire.roll();
	frontRightTire.roll();
    backLeftTire.roll();
    backRightTire.roll();
}

교체하기 전에는 Tire 객체의 roll()메소드가 호출되지만, HankookTire, KumhoTire가 roll()메소드를 재정의하고있어 교체하고나서는 HankookTire, KumhoTire의 roll()메소드가 실행되어 결과가 달라진다.

이처럼 자동 타입 변환을 이용해 Tire 필드값을 교체함으로써 Car의 run()메소드를 수정하지않아도 다양한 roll()메소드의 실행결과를 얻게 된다 = 필드의 다형성

예제를 작성하며 지금까지의 설명을 눈으로 보자.

Tire 클래스

package sec01.exam07.pack2;

public class Tire {
	
	//필드
	public int maxRotation; //최대 회전수(타이어수명)
	public int accumulatedRotation; //누적 회전수 
	public String location; // 타이어 위치 
	
	//생성자 
	public Tire(String location, int maxRotation) {
		this.location = location; //초기화 
		this.maxRotation = maxRotation; //초기화 
	}
	
	//메소드 
	public boolean roll() {
		++accumulatedRotation;
		if(accumulatedRotation < maxRotation) {
			System.out.println(location + " Tire 수명: " + (maxRotation - accumulatedRotation) + "회"); //정상 회전(누적<최대)일 경우 실행 
			return true;
		} else {
			System.out.println("*** " + location + " Tire 펑크 ***"); //펑크(누적 = 최대)일 경우 실행 
			return false;
		}
	}
	

}

Car 클래스

타이어 번호는 타이어를 교체할 때 어떤 위치의 타이어인지 알 수 있도록 하기위해 사용된다.

package sec01.exam07.pack2;

public class Car {
	
	//필드 
	Tire frontLeftTire = new Tire("앞왼쪽" ,6);
    Tire frontRightTire = new Tire("앞오른쪽",2);
    Tire backLeftTire = new Tire("뒤왼쪽",3);
    Tire backRightTire = new Tire("뒤오른쪽",4);
    
    //생성자
    
    //메소드
    //모든 타이어를 1회 회전시키기 위해 각 Tire객체의 roll()메소드 호출. false를 리턴하는 roll()이 있을 경우 stop()메소드를 호출하고 해당 타이어 번호를 리턴 
    int run() {
    	System.out.println("[자동차가 달립니다.]");
    	if(frontLeftTire.roll() ==false) {stop(); return 1;}
    	if(frontRightTire.roll() ==false) {stop(); return 2;}
    	if(backLeftTire.roll() ==false) {stop(); return 3;}
    	if(backRightTire.roll() ==false) {stop(); return 4;}
    	return 0;
    }
    
    void stop() {
    	System.out.println("[자동차가 멈춥니다.]");
    }
}

HankookTire, KumhoTire 클래스

Tire 클래스를 상속받는다.

package sec01.exam07.pack2;

public class HankookTire extends Tire{
	//필드 
	
	//생성자
	public HankookTire(String location, int maxRotation) {
		super(location, maxRotation);
	}
	
	//메소드
	@Override
	public boolean roll() {
		++accumulatedRotation;
		if(accumulatedRotation < maxRotation) {
			System.out.println(location + " HankookTire 수명: " + (maxRotation - accumulatedRotation) + "회"); //정상 회전(누적<최대)일 경우 실행 
			return true;
		} else {
			System.out.println("*** " + location + " HankookTire 펑크 ***"); //펑크(누적 = 최대)일 경우 실행 
			return false;
		}
	}

}
package sec01.exam07.pack2;

public class KumhoTire extends Tire {
	//필드 
	
	//생성자
	public KumhoTire(String location, int maxRotation) {
		super(location, maxRotation);
	}
			
	//메소드
	@Override
	public boolean roll() {
		++accumulatedRotation;
		if(accumulatedRotation < maxRotation) {
			System.out.println(location + " KumhoTire 수명: " + (maxRotation - accumulatedRotation) + "회"); //정상 회전(누적<최대)일 경우 실행 
			return true;
		} else {
			System.out.println("*** " + location + " KumhoTire 펑크 ***"); //펑크(누적 = 최대)일 경우 실행 
			return false;
		}
	}	

}

CarExample 클래스

package sec01.exam07.pack2;

public class CarExample {

	public static void main(String[] args) {
		Car car = new Car();
		
		for(int i=1; i<=5; i++) {
			int problemLocation = car.run();
			
			switch(problemLocation) {
				case 1 : 
					System.out.println("앞왼쪽 HankookTire로 교체");
					car.frontLeftTire = new HankookTire("앞왼쪽",15);
					break;
				case 2:
					System.out.println("앞오른쪽 KumhoTire로 교체");
					car.frontRightTire = new KumhoTire("앞오른쪽",13);
					break;
				case 3 :
					System.out.println("뒤왼쪽 HankookTire로 교체");
					car.backLeftTire = new HankookTire("뒤왼쪽",14);
					break;
				case 4 :
					System.out.println("뒤오른쪽 KumhoTire로 교체");
					car.backRightTire = new KumhoTire("뒤오른쪽",17);
					break;
			}
			System.out.println("--------------------"); //1회전시 구분 
		}
	}
}

매개 변수의 다형성

자동 타입 변환은 필드의 값을 대입할 때에도 발생하지만, 주로 메소드를 호출할 때 많이 발생한다.
메소드를 호출할 때에는 매개 변수의 타입과 동일한 매개값을 지정하는 것이 정석이지만, 매개값을 다양화하기 위해 매개 변수에 자식 객체를 지정할 수도 있다.

예를 들어, Driver 클래스에는 drive()메소드가 정의되어있는데, Vehicle 타입의 매개변수가 선언되어있다.

class Driver {
	void drive(Vehicle vehicle) {
    	vehicle.run();
    }
}

drive() 메소드를 정상적으로 호출하면 다음과 같다.

Driver driver = new Driver();
Vehicle vehicle = new Vehicle();
driver.drive(vehicle);

만약 Vehicle의 자식 클래스인 Bus 객체를 drive() 메소드의 매개값으로 넘겨준다면?

자동타입변환이 발생한다.

매개 변수의 타입이 클래스일 경우, 해당 클래스의 객체뿐만 아니라 자식 객체까지도 매개값으로 사용할 수 있다는것은 매우 중요하다!
즉, 매개값으로 어떤 자식 객체가 제공되느냐에 따라 메소드의 실행 결과는 다양해질 수 있다.
자식 객체가 부모의 메소드를 재정의했다면 메소드 내부에서 재정의된 메소드를 호출함으로써 실행결과는 다양해진다.

예제를 보자.

Vehicle 클래스

package sec02.exam04;

public class Vehicle {
	public void run() {
		System.out.println("차량이 달립니다.");
	}
}

Driver 클래스

Vehicle을 이용

package sec02.exam04;

public class Driver {
	public void drive(Vehicle vehicle) {
		vehicle.run();
	}
}

Bus, Taxi 클래스

Vehicle 상속받음

package sec02.exam04;

public class Bus extends Vehicle{
	@Override
	public void run() {
		System.out.println("버스가 달립니다.");
	}
}
package sec02.exam04;

public class Taxi extends Vehicle {
	@Override
	public void run() {
		System.out.println("택시가 달립니다.");
	}
}

DriverExample 클래스

package sec02.exam04;

public class DriverExample {

	public static void main(String[] args) {
		Driver driver = new Driver();
		
		Bus bus = new Bus();
		Taxi taxi = new Taxi();
		
		driver.drive(bus); //자동타입 변환 : Vehicle vehicle = bus;
		driver.drive(taxi); //자동타입 변환 : Vehicle vehicle = taxi;

	}

}

이와 같이 매개값의 자동 타입 변환과 메소드 재정의를 이용해 매개 변수의 다형성을 구현할 수 있다.

자동 타입 변환을 이용해 필드와 매개변수의 다형성을 구현한다.

강제 타입 변환 (casting)

강제 타입 변환은 부모 타입을 자식 타입으로 변환하는것을 말한다.
모든 부모 타입을 자식 타입으로 강제 변환할 수 있는것은 아니다.

자식 타입이 부모 타입으로 자동 타입 변환한 후 다시 자식 타입으로 변환할 때 강제 타입 변환을 사용할 수 있다.

캐스팅연산자 () 를 사용해서 강제 타입 변환할 수 있다.

예를들어, 다음 코드와 같이 Child 객체가 Parent 타입으로 자동 변환된 상태에서 원래 Child로 강제 변환할 수 있다.

Parent parent = new Child(); //자동 타입 변환
Child child = (Child) parent; //강제 타입 변환 

자식타입이 부모 타입으로 자동 타입 변환하면, 부모에 선언된 필드와 메소드만 사용가능하다는 제약 사항이 따른다.
만약 자식에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해 다시 자식 타입으로 변환한 다음 자식의 필드와 메소드를 사용하면 된다.

field2와 method3() 메소드는 Child 타입에만 선언되어 있으므로 Parent타입으로 자동 타입 변환하면 사용할 수 없다.
이를 사용하고싶다면 다시 Child 타입으로 강제변환해야한다.

객체 타입 확인

강제 타입 변환은 자식 타입이 부모 타입으로 변환되어 있는 상태에서만 가능하기 때문에 처음부터 부모 타입으로 생성된 객체는 자식 타입으로 변환할 수 없다.

Parent parent = new Parent();
Child child = (Child) parent; //강제 타입 변환할 수 없음

그렇다면 부모 변수가 참조하는 객체가 부모객체인지 자식 객체인지 확인하는 방법은 없을까?
어떤 객체가 어떤 클래스의 인스턴스인지 확인하기 위해 instanceof연산자를 사용한다.

instanceof연산자의 좌항에는 객체가오고, 우항에는 타입이 오는데, 좌항의 객체가 우항의 인스턴스이면, 즉 우항의 타입으로 객체가 생성되었다면 true를 리턴하고 그렇지않으면 false를 리턴한다.

boolean result = 좌항(객체) instanceof 우항(타입)

instanceof연산자는 주로 매개값의 타입을 조사할 때 사용된다.
메소드 내에서 강제 타입 변환이 필요할 경우 반드시 매개값이 어떤 객체인지 instanceof연산자로 확인하고 안전하게 강제 타입 변환해야한다.

public void method(Parent parent) {
	if(parent instanceof Child) { //Parent 매개변수가 참조하는 객체가 Child인지 조사
    	Child child = (Child)parent;
    }
}

만약 타입을 확인하지 않고 강제 타입 변환을 시도하면, ClassCastException이 발생할 수 있다.

Parent 클래스

package sec02.exam06;

public class Parent {

}

Child 클래스

package sec02.exam06;

public class Child extends Parent {

}

InstanceOfExample 클래스

package sec02.exam06;

public class InstanceOfExample {
	public static void method1(Parent parent) {
		if(parent instanceof Child) { //Child 타입으로 변환이 가능한지 확인 
			Child child = (Child) parent;
			System.out.println("method1 - Child로 변환 성공");
		} else {
			System.out.println("method1 - Child로 변환되지 않음");
		}
	}

	
	public static void method2(Parent parent) {
		Child child = (Child)parent; //classCastException발생 가능성이 있음 
		System.out.println("method2 - Child로 변환 성공");
	}
	public static void main(String[] args) {
		Parent parentA = new Child();
		method1(parentA); //Child 객체를 매개값으로 전달 
		method2(parentA); //Child 객체를 매개값으로 전달 
		
		Parent parentB = new Parent();
		method1(parentB); //Parent 객체를 매개값으로 전달 
		method2(parentB); //예외발생, Parent 객체를 매개값으로 전달 
	}
}

예외가 발생하면 프로그램은 즉시 종료되기 때문에 method1()과 같이 강제 타입 변환을 하기 전에 instanceof 연산자로 변환시킬 타입의 객체인지 조사해서 잘못된 매개값으로 인해 프로그램이 종료되는것을 막아야한다.


출처
혼자 공부하는 자바

0개의 댓글