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

Benjamin·2023년 5월 17일
0

혼공자

목록 보기
25/27

다형성 구현 위해 아래 2가지 기능이 필요

  • 메소드 재정의
  • 타입변환

인터페이스 역시 위 두가지 기능을 제공하므로 상속과 더불어 다형성을 구현하는데 많이 사용된다.

  • 상속 = 같은 종류의 하위 클래스를 만드는 기술
  • 인터페이스 = 사용 방법이 동일한 클래스를 만드는 기술

-> 개념상 차이가 있으나 둘 다 다형성을 구현하는 방법은 비슷

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

  • 인터페이스의 다형성
    인터페이스를 사용해 메소드를 호출하도록 코딩했다면, 구현 객체를 손쉽고 빠르게 교체 가능하다
    프로그램 소스 코드는 변함 없는데, 구현 객체를 교체함으로써 프로그램의 실행결과가 다양해진다.

다음과 같이 I 인터페이스를 이용해 프로그램을 개발하는 상황을 보자
I인터페이스를 구현한 클래스로 처음에는 A클래스를 선택했는데, 테스트를 해보니 A 클래스에 문제가 있는것을 알았다.
그래서 B클래스와 교체한 후 단 한줄만 수정해서 프로그램을 재실행할 수 있다.

자동 타입 변환 (promotion)

구현 객체가 인터페이스 타입으로 변환되는 것
프로그램 실행 도중 자동적으로 타입 변환이 일어나는 것

인터페이스 변수 = 구현객체; // 구현객체가 변수 타입(인터페이스)으로 자동 타입 변환

인터페이스 구현 클래스를 상속해 자식 클래스를 만들었다면 자식 객체 역시 인터페이스 타입으로 자동 타입 변환할 수 있다.

자동 타입 변환을 이용하면 필드의 다형성과 매개 변수의 다형성을 구현할 수 있다.
필드와 매개변수 타입을 인터페이스로 선언하면 다양한 구현 객체를 대입해 실행결과를 다양하게 만들 수 있다.

필드의 다형성

다음 그림은 상속에서 다형성을 설명할 때 보여준 그림과 유사하지만, 타이어가 클래스 타입이 아닌 인터페이스 타입이라는 점과 한국 타이어와 금호 타이어는 자식 클래스가 아닌 구현 클래스라는 차이점이 있다.

한국 타이어와 금호 타이어는 타이어 인터페이스를 구현했기 때문에 모두 타이어 인터페이스에 있는 메소드를 가지고 있다.
따라서 인터페이스로 동일하게 사용할 수 있는 교체 가능한 객체에 해당한다.

자동차 설계시 필드 타입으로 타이어 인터페이스를 선언하면 필드값으로 한국 타이어 또는 금호 타이어 객체를 대입할 수 있다.
자동 타입 변환이 일어나기때문에 아무런 문제가 없다.

public class Car {
	Tire frontLeftTire = new HankookTire(); //인터페이스 타입 필드 선언과 초기 구현 객체 대입
	Tire frontRightTire = new HankookTire();
	Tire backLeftTire = new HankookTire();
	Tire backRightTire = new HankookTire();
}

Car 객체 생성 후, 초기값으로 대입한 구현 객체 대신 다른 구현 객체를 대입할 수도 있다.
이는 타이어 교체에 해당한다.

Car myCar = new Car();
myCar.frontLeftTire = new KumhoTire();
myCar.frontRightTire = new KumhoTire();

frontLeftTire, frontRightTire에 어떤 타이어 구현 객체가 저장되어도 Car 객체는 타이어 인터페이스에 선언된 메소드만 사용하므로 전혀 문제가 되지 않는다.

다음은 Car 객체의 run() 메소드에서 타이어 인터페이스에 선언된 roll() 메소드를 호출한다.

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

frontLeftTire, frontRightTire을 교체하기 전에는 HankookTire객체의 roll()메소드가 호출되지만, KumhoTire로 교체된 후에는 KumhoTire객체의 roll() 메소드가 호출된다.

Car의 run()메소드를 수정하지 않아도 다양한 roll() 메소드의 실행결과를 얻을 수 있게된다.
이게 바로 필드의 다형성이다.

매개 변수의 다형성

자동 타입 변환은 필드의 값을 대입할 때에도 발생하지만, 주로 메소드를 호출할 때 많이 발생한다.
매개값을 다양화하기 위해 상속에서는 매개 변수를 부모 타입으로 선언하고 호출할 때에는 자식 객체를 대입했다.
이번에는 매개 변수를 인터페이스 타입으로 선언하고 호출할 때에는 구현 객체를 대입한다.

아래 예시를 보자.

public interface Vehicle {
	public void run();
}
public class Driver {
	public void drive(Vehicle vehicle) {
    	vehicle.run(); // 파라미터에 Vehicle의 구현객체가 오면 구현객체의 run() 메소드가 실행된다
    }
}

만약 Bus가 구현 클래스라면 다음과 같이 Driver의 drive메소드를 호출할 때 Bus 객체를 생성해서 매개값으로 줄 수 있다.

Vehicle을 구현한 Bus 객체가 매개값으로 사용되면 자동 타입 변환이 발생한다.

매개 변수 타입이 인터페이스일 경우 어떤 구현 객체도 매개값으로 사용할 수 있고, 어떤 구현 객체가 제공되느냐에 따라 메소드의 실행결과는 다양해질 수 있다.
이것이 인터페이스 매개 변수의 다형성이다.

강제 타입 변환(casting)

구현 객체가 인터페이스 타입으로 자동 타입 변환하면, 인터페이스에 선언된 메소드만 사용 가능하다는 제약사항이 따른다.

예를 들어 아래와 같이 인터페이스에는 3개의 메소드가 선언되어있고, 클래스에는 5개의 메소드가 선언되어 있다면, 인터페이스로 호출 가능한 메소드는 3개뿐이다.

하지만 경우에 따라 구현 클래스에 선언된 필드와 메소드를 사용해야 할 경우도 발생한다.
이때 강제 타입 변환을 해서 다시 구현 클래스 타입으로 변환한 다음, 구현 클래스의 필드와 메소드를 사용할 수 있다.

객체 타입 확인

강제 타입 변환은 구현 객체가 인터페이스 타입으로 변환되어 있는 상태에서 가능하다.
그러나 어떤 구현 객체가 변환되어 있는지 알 수 없는 상태에서 무작정 강제 타입 변환할 경우 ClassCastException이 발생할 수 있다.

예를 들어 다음과 같이 Taxi 객체가 인터페이스로 변환되어 있을 경우, Bus 타입으로 강제 타입 변환하면 구현 클래스 타입이 다르므로 ClassCastException이 발생한다.

Vehicle vehicle = new Taxi();
Bus bus = (Bus) vehicle;

마찬가지로 메소드의 매개변수를 인터페이스로 선언한 경우, 어떤 구현 객체가 지정될지 모르는 상황에서 다음과 같이 매개값을 Bus로 강제 타입 변환하면 ClassCastException이 발생할 수 있다.

public void drive(Vehicle vehicle) {
	Bus bus = (Bus) vehicle;
    bus.checkFare(); // Vehicle에는 없고 Bus에만 있는 메소드
    vehicle.run();
}

그렇다면 어떤 구현 객체가 인터페이스 타입으로 변환되었는지 확인하는 방법은 뭘까?
상속에서 객체 타입을 확인하기 위해 instanceof 연산자를 사용했다. 이 연산자는 인터페이스 타입에서도 사용할 수 있다.

예를 들어, Vehicle 인터페이스 타입으로 변환된 객체가 Bus인지 확인하려면 다음과 같이 작성하면 된다.

if(vehicle instanceof Bus) {
	Bus bus = (Bus) vehicle;
}

인터페이스 타입으로 자동 타입 변환된 매개값을 메소드 내에서 다시 구현 클래스 타입으로 강제 타입 변환해야 한다면 반드시 매개값이 어떤 객체인지 instanceof 연산자로 확인하고, 안전하게 강제 타입 변환 해야한다.

public class Driver {
	public void drive(Vehicle vehicle) {
    	if(vehicle instanceof Bus) { //vehicle 매개 변수가 참조하는 객체가 Bus인지 조사
        	Bus bus = (Bus) vehicle; // Bus 객체일 경우 안전하게 강제 타입 변환
            bus.checkFare(); // Bus 타입으로 강제 타입 변환 하는 이유 (Vehicle에는 없고 Bus에만 있는 메소드 사용 위해)
        }
    }
}

인터페이스 상속

인터페이스도 다른 인터페이스를 상속할 수 있다.
인터페이스는 클래스와 달리 다중 상속을 허용한다.

extends 키워드 뒤에 상속할 인터페이스들을 나열할 수 있다.

public interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2 {...}

하위인터페이스를 구현하는 클래스는 하위 인터페이스의 메소드뿐만 아니라 상위 인터페이스의 모든 추상 메소드에 대한 실체 메소드를 가지고있어야 한다.
그렇기 때문에 구현 클래스로부터 객체를 생성한 후 다음과 같이 하위 및 상위 인터페이스 타입으로 변환이 가능하다.

하위인터페이스 변수 = new 구현클래스(...);
상위인터페이스1 변수 = new 구현클래스(...);
상위인터페이스2 변수 = new 구현클래스(...);

하위 인터페이스로 타입 변환되면 상위 및 하위 인터페이스에 선언된 모든 메소드를 사용할 수 있으나, 상위 인터페이스로 타입 변환되면 상위 인터페이스에 선언된 메소드만 사용가능하고 하위 인터페이스에 선언된 메소드는 사용할 수 없다.

예를 들어, 아래와 같이 인터페이스가 상속 관계에 있다고 가정해보자.

InterfaceC인터페이스 변수는 methodA(), methodB(), methodC()를 모두 호출할 수 있지만, InterfaceA와 InterfaceB 변수는 각각 methodA(), methodB()만 호출가능하다.

예제

public interface InterfaceA {
	public void methodA();
}
public interface InterfaceB {
	public void methodB();
}
public interface InterfaceC extends InterfaceA, InterfaceB {
	public void methodC();
}
public class ImplementationC implements InterfaceC {

//InterfaceA와 InterfaceB의 실체 메소드도 있어야 함
	public void methodA() {
    	System.out.println("ImplementationC-methodA() 실행");
    }
    
    public void methodB() {
    	System.out.println("ImplementationC-methodB() 실행");
    }
    
    public void methodC() {
    	System.out.println("ImplementationC-methodC() 실행");
    }
}
public class Example {
	public static void main(String[] args) {
		ImplementationC impl = new ImplementationC();
		
		InterfaceA ia = impl;
		ia.methodA(); //methodA()만 호출 가능
		
		InterfaceB ib = impl;
		ib.methodB(); //methodB()만 호출 가능
		
		InterfaceC ic = impl;
		ic.methodA();
		ic.methodB();
		ic.methodC();
	}
}

출처
혼자 공부하는 자바

0개의 댓글