[Java 21] 자바 - 인터페이스(2)

학습목표

  1. 인터페이스의 타입 변환과 다형성을 정확히 이해한다.
  2. 인터페이스 상속에 대해 이해한다.

타입 변환과 다형성

프로그램을 개발할 때 인터페이스를 사용해서 메소드를 호출하도록 코딩을 했다면, 구현 객체를 교체하는 것은 매우 손쉽고 빠르게 할 수 있습니다. 프로그램 소스 코드는 변함이 없는데, 구현 객체를 교체함으로써 프로그램의 실행 결가가 다양해질 수 있습니다. 이것이 인터페이스의 다형성입니다.

자동 타입 변환(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();

frintLeftTire와 frontRightTire를 교체하기 전에는 HankookTire 객체의 roll() 메소드가 호출되지만, KumhoTire 객체로 변경된 이후 KumhoTire 객체의 roll() 메소드가 호출됩니다.

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


예제 1

[Tire] 인터페이스

public interface Tire {
	public void roll();
}

[HankookTire] 구현 클래스

public class HankookTire implements Tire{
	@Override
	public void roll() {
		System.out.println("한국 타이어가 굴러갑니다.");
	}
}

[KumhoTire] 구현 클래스

public class KumhoTire implements Tire {
	@Override
	public void roll() {
		System.out.println("금호 타이어가 굴러갑니다.");
	}
}

[Car] 필드 다형성

public class Car {
	Tire frontLeftTire = new HankookTire();
	Tire frontRightTire = new HankookTire();
	Tire backLeftTire = new HankookTire();
	Tire backRightTire = new HankookTire();

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

[CarMain] 실행 클래스

public class CarMain {
	public static void main(String[] args) {
		Car car = new Car();
		car.run();
		System.out.println();
		
		car.frontLeftTire = new KumhoTire();
		car.frontRightTire = new KumhoTire();
		
		car.run();
		System.out.println();
	}
}


인터페이스 배열로 구현 객체 관리

위의 예제를 다음과 같이 배열로 관리할 수도 있습니다.

Tire[] tires = {
  new HankookTire(),
  new HankookTire(),
  new HankookTire(),
  new HankookTire()
} 

frontLeftTire 는 tires[0], frontRightTire는 tires[1], backLeftTire는 tires[2], backRightTire는 tires[3]과 같이 인덱스로 표현되므로 대입이나 제어문에서 활용하기 쉽습니다.

tires[1] = new KumhoTire(); // 앞오른쪽 바퀴를 금호타이어로 교체

roll() 메소드를 호출하는 Car 클래스의 run()메소드는 다음과 같이 간단하게 작성이 가능해집니다.

void run() {
  for(Tire tire : tires) {
    tire.roll();
  }
}


예제 2

예제 1을 배열로 변경해서 실행해보겠습니다.

[Car] 필드 다형성

public class Car {
	Tire[] tires = {
			  new HankookTire(),
			  new HankookTire(),
			  new HankookTire(),
			  new HankookTire()
			};
	
	void run() {
		for (Tire tire : tires) {
			tire.roll();
		}
	}
}


[CarMain] 필드 다형성 실행 클래스

public class CarMain {
	public static void main(String[] args) {
		Car car = new Car();
		car.run();

		car.tires[0] = new KumhoTire();
		car.tires[1] = new KumhoTire();
		car.run();
	}
}


매개변수의 다형성

매개값을 다양화하기 위해서 상속에서는 매개 변수를 부모 타입으로 선언하고 호출할 때에는 자식 객체를 대입했습니다. 이번에는 매개변수를 인터페이스 타입으로 선언하고 호출할 때는 구현 객체를 대입합니다.

[Driver]

public class Driver() {
  public void drive(Vehicle vehicle) {    //Vehicle 타입의 매개 변수 선언
    vehicle.run();
  }
}

[Vehicle] 인터페이스

public interface Vehicle {
  public void run();
}
Driver driver = new Driver();

Bus bus = new Bus();

driver.drive( bus );  // 자동 타입 변환 발생 Vehicle vehicle = bus;

예제

[Driver] 매개 변수의 인터페이스화

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

[Vehicle] 인터페이스

public interface Vehicle {
	public void run();
}

[Bus] 구현 클래스

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

[Taxi]] 구현 클래스

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

[DriverMain] 매개 변수의 다형성 실행클래스

public class DriverMain {
	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)

구현 객체가 인터페이스 타입으로 자동 변환하면, 인터페이스에 선언된 메소드만 사용 가능하다는 제약 사항이 따릅니다. 예를 들어, 인터페이스에는 세 개의 메소드가 선언되어 있고, 클래스에는 다섯개의 메소드가 선언되어 있다면, 인터페이스로 호출 가능한 메소드는 세 개뿐입니다.

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

구현클래스 변수 = (구현클래스) 인터페이스 변수;


예제

[Vehicle] 인터페이스

public interface Vehicle {
	public void run();
}

[Bus] 구현 클래스

public class Bus implements Vehicle{
	@Override
	
	public void run() {
		System.out.println("버스 달립니다.");
	}
	
	public void checkFare() {
		System.out.println("요금을 확인합니다.");
	}
}

[VeicleMain] 강제 타입 변환

public class VehicleMain {
	public static void main(String[] args) {
		Vehicle vc = new Bus();
		
		vc.run();
		
		Bus bus = (Bus) vc;
		
		bus.run();
		bus.checkFare();
	}
}

실행결과

버스 달립니다.
버스 달립니다.
요금을 확인합니다.


객체 타입 확인(instanceof)

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

Vehicle vc = new Taxi();  //Taxi 객체가 인터페이스로 변환되어 있음.
Bus bus = (Bus) vc;
public void drive(Vehicle vc) {
	Bus bus = (Bus) vehicle;
	bus.checkFare();
	vehicle.run()
}

실행결과

java.lang.ClassCastException: day0503.
Taxi cannot be cast to day0503.Bus at day0503.VehicleMain.main(VehicleMain.java:9)

이를 방지하기 위해 어떤 구현 객체가 인터페이스 타입으로 변환되었는지 확인하는 연산자로 instanceof를 사용하면 된다.

public class Driver {
	public void driver(Vehicle vc) {
		if(vc instanceof Bus) {		// vc 매개변수가 참조하는 객체가 Bus인지 조사
			Bus bus = (Bus) vc;		// Bus 객체일 경우 안전하게 강제 타입 변환
			bus.checkFare();
		}
		vc.run();
	}
}

인터페이스 상속

인터페이스도 다른 인터페이스를 상속할 수 있을 뿐만 아니라 다중 상속도 허용됩니다.

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

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

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

예제

[InterfaceA] 부모 인터페이스

public interface InterfaceA {
	public void methodA();
}

[InterfaceB] 부모 인터페이스

public interface InterfaceB {
	public void methodB();
}

[InterfaceC] 하위 인터페이스

public interface InterfaceC extends InterfaceA, InterfaceB {
	public void methodC();	
}

[ImplementationC] 하위 인터페이스 구현

public class ImplementationC implements InterfaceC{
	public void methodA() {
		System.out.println("ImplementationC-methodA() 실행");
	}
	public void methodB() {
		System.out.println("ImplementationC-methodB() 실행");
	}
	public void methodC() {
		System.out.println("ImplementationC-methodC() 실행");
	}
}

[Example] 호출 가능 메소드

public class Example {
	public static void main(String[] args) {
		ImplementationC impl = new ImplementationC();

		InterfaceA ia = impl;
		ia.methodA();
		System.out.println();

		InterfaceB ib = impl;
		ib.methodB();
		System.out.println();

		InterfaceC ic = impl;
		ia.methodA();
		ib.methodB();
		ic.methodC();
	}
}

실행결과

ImplementationC-methodA() 실행

ImplementationC-methodB() 실행

ImplementationC-methodA() 실행
ImplementationC-methodB() 실행
ImplementationC-methodC() 실행

디퐅트 메소드와 인터페이스 확장

디폴트 메소드의 필요성

인터페이스에서 디폴트 메소드를 허용한 이유는 기존 인터페이스를 확장해서 새로운 기능을 추가하기 위해서 입니다. 기존 인터페이스의 이름과 추상 메소드를 변경 없이 디폴트 메소드만 추가할 수 있기 때문에 이전에 개발한 구현 클래스를 그대로 사용할 수 있으면서 새로 개발하는 클래스는 디폴트 메소드를 활용할 수 있다는 장점이 있습니다.

인터페이스

예제

[MyInterface] 수정 인터페이스

public interface MyInterface {
	public void method1();

	// 디폴트 메소드 -> 수정된 부분
	public default void method2() {
		System.out.println("MyInterface-method2 실행");
	}
}

[MyClassA] 기존 구현 클래스

public class MyClassA implements MyInterface {
	@Override
	public void method1() {
		System.out.println("MyClass-method1() 실행");
	}
}

[MyClassB] 새로운 구현 클래스

public class MyClassB implements MyInterface {
	@Override
	public void method1() {
		System.out.println("MyClass-method1() 실행");
	}

	@Override
	public void method2() {
		System.out.println("MyClass-method2() 실행");
	}
}

[DefaultMethodMain] 디폴트 메소드 사용

public class DefaultMethodMain {
	public static void main(String[] args) {
		MyInterface mi1 = new MyClassA();
		mi1.method1();
		mi1.method2();
		
		MyInterface mi2 = new MyClassB();
		mi2.method1();
		mi2.method2();		
	}
}

실행결과

MyClass-method1() 실행
MyInterface-method2 실행
MyClass-method1() 실행
MyClass-method2() 실행


디폴트 메소드가 있는 인터페이스 상속

자식 인터페이스에서 디폴트 메소드 활용법  
  1. 디폴트 메소드를 단순히 상속 받음
  2. 디폴트 메소드를 재정의(Override)해서 실행 내용 변경
  3. 디폴트 메소드를 추상 메소드로 재선언

태그:

카테고리:

업데이트:

댓글남기기