[Java] 타입 변환과 다형성 (2)
하나의 배열로 객체 관리
이전 예제에서 Car 클래스에 4개의 타이어 객체를 4개의 필드로 각각 저장했다. 우리는 동일한 타입의 값들은 배열로 관리하는 것이 유리하다는 것을 알고 있다. 그렇다면 타이어 객체들도 타이어 배열로 관리하는 것이 더 깔끔하게 만들어 줄 것이다.
class Car {
Tire[] tires = {
new Tire("앞왼쪽", 6),
new Tire("앞오른쪽", 2),
new Tire("뒤왼쪽", 3),
new Tire("뒤오른쪽", 4)
};
}
frontLeftTire는 tires[0], frontRightTire는 tires[1], ... 처럼 인덱스로 표현되므로 대입이나 제어문에서 활용하기 매우 쉽다. 예를 들어 인덱스 1을 이용해서 앞오른쪽 타이어를 KumhoTire로 교체하기 위해 다음과 같이 작성할 수 있다.
tires[1] = new KumhoTire("앞오른쪽", 13);
tires 배열의 각 항목은 Tire 타입이므로 자식 객체인 KumhoTire를 대입하면 자동 타입 변환이 발생하기 때문에 아무런 문제가 없다. 배열의 타입은 Tire이지만 실제 저장 항목이 Tire의 자식 객체라면 모두 가능하다. 상속 관계에 있는 객체들을 배열로 관리하면 제어문에서 가장 많이 혜택을 본다.
매개 변수의 다형성
자동 타입 변환은 필드의 값을 대입할 때에도 발생하지만, 주로 메소드를 호출할 때 많이 발생한다. 메소드를 호출할 때에는 매개 변수의 타입과 동일한 매개값을 지정하는 것이 정석이지만, 매개값을 다양화하기 위해 매개 변수에 자식 타입 객체를 지정할 수도 있다. 예를 들어 다음과 같이 Driver라는 클래스가 있다. 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() 메소드의 매개값으로 넘겨준다면 어떻게 될까?
Driver driver = new Driver();
Bus bus = new bus();
driver.drive(bus);
drive() 메소드는 Vehicle 타입을 매개 변수로 선언했지만, Vehicle을 상속받는 Bus 객체가 매개값으로 사용되면 자동 타입 변환이 발생한다.
우리는 여기서 매우 중요한 것을 하나 알게 되었다. 매개 변수의 타입이 클래스일 경우, 해당 클래스의 객체 뿐만아니라 자식 객체까지도 매개값으로 사용할 수 있다는 것이다. 매개값으로 어떤 자식 객체가 제공되느냐에 따라 메소드의 실행 결과는 다양해질 수 있다. 자식 객체가 부모 메소드를 오버라이딩했다면 메소드 내부에서 오버라이딩된 메소드를 호출함으로써 메소드의 실행 결과는 다양해진다.
예제를 보면서 지금까지 설명했던 내용을 눈으로 확인해보자.
// Vehicle.java
public class Vehicle {
public void run() {
System.out.println("차량이 달립니다.");
}
}
// Driver.java
public class Driver {
public void driver(Vehicle vehicle) {
vehicle.run();
}
}
// Bus.java
public class Bus extends Vehicle {
@Override
public void run() {
System.out.println("택시가 달립니다.");
}
}
// DriverExample.java
public class DriverExample {
public static void main(String[] args) {
Driver driver = new Driver();
Bus bus = new Bus();
Taxi taxi = new Taxi();
driver.drive(bus);
driver.drive(taxi);
}
}
강제 타입 변환 (Casting)
강제 타입 변환(Casting)은 부모 타입을 자식 타입으로 변환하는 것을 말한다. 그렇다고 해서 모든 부모 타입을 자식 클래스 타입으로 강제 변환할 수 있는 것은 아니다. 자식 타입이 부모 타입으로 자동 변환한 후, 다시 자식 타입으로 변환할 때 강제 타입 변환을 사용할 수 있다.
자식 클래스 변수 = (자식클래스) 부모클래스타입;
자식 타입이 부모 타입으로 자동 변환하면, 부모 타입에 선언된 필드와 메소드만 사용 가능하다는 제약 사항이 따른다 .만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식 타입으로 변환한 다음 자식 타입의 필드와 메소드를 사용하면 된다.
// Parent.java
public class Parent {
public String field1;
public void method1(); {
System.out.println("Parent-method1()");
}
public void method2() {
System.out.println("Parent-method2()");
}
}
// Child.java
public class Child extends Parent {
public String field2;
public void mehtod3() {
System.out.println("Child-method3()");
}
}
// ChildExample.java
public class ChildExample {
public static void main(String[] args) {
Parent parent = new Child(); // 자동 타입 변환
parent.field1 = "data1";
parent.method1();
parent.method2();
/*
parent.field2 = "data2"; // (x)
parent.method3(); // (x)
*/
Child child = (Child) parent; // 강제 타입 변환
child.field2 = "yyy";
child.method3();
}
}
객체 타입 확인 (instanceof)
강제 타입 변환은 자식 타입이 부모 타입으로 변환되어 있는 상태에서만 가능하기 때문에 다음과 같이 부모 타입의 변수가 부모 객체를 참조할 경우 자식 타입으로 변환할 수 없다.
Parent parent = new Parent();
Child child = (Child) parent; // 강제 타입 변환을 할 수 없다.
어떤 객체가 어떤 클래스의 인스턴스인지 확인하려면 instanceof 연산자를 사용할 수 있다. instanceof 연산자의 좌항은 객체가 오고, 우항은 타입이 오는데, 좌항의 객체가 우항의 인스턴스이면, 즉 우항의 타입으로 객체가 생성되었다면 true를 산출하고 그렇지 않으면 false를 산출한다.
boolean result = 좌항(객체) instanceof 우항(타입)
instanceof 연산자는 매개값의 타입을 조사할 때 주로 사용된다. 메소드 내에서 강제 타입 변환이 필요할 경우 반드시 매개값이 어떤 객체인지 instanceof 연산자로 확인하고 안전하게 강제 타입 변환을 해야한다.
public void method(Parent parent) {
if(parnet instanceof Child) { // Parent 매개변수가 참조하는 객체가 Child인지 조사
Child child = (Child) parent;
}
}
만약 타입을 확인하지 않고 강제 타입 변환을 시도한다면 ClassCastException 예외가 발생할 수 있다.