인터페이스
인터페이스는 일종의 추상클래스라고 볼 수 있다. 추상클래스처럼 추상 메소드를 갖지만 추상화 정도가 추상클래스보다 높아서 추상클래스와 달리 몸통을 가진 일반 메소드와 멤버변수를 가질 수 없다.
오직 추상 메소드와 상수만을 멤버로 가질 수 있으며, 다른 클래스를 작성하는데 도움을 줄 목적으로 작성된다.
인터페이스의 작성
인터페이스를 작성하는 것은 키워드를 class 대신 interface를 사용한다는 것 외에는 클래스와 같다. 또한 접근제어자로 public와 default를 사용할 수 있다.
인터페이스에는 모든 멤버변수는 public static final이어야 하며, 모든 메소드는 public abstract이어야 한다는 제약사항이 있지만 이들은 생략이 가능한 부분이다. 그 이유는 인터페이스에 정의된 모든 멤버에 적용되는 사항이기 때문이며, 생략된 제어자는 컴파일 시에 컴파일러가 자동으로 추가해준다.
interface Ex{
public static abstract final int Spade = 4;
final int Diamond = 3; // public static final int Diamond = 3;
static int Heart = 2; // public static final int Heart = 3;
int Clover = 1; // public static final int Clover = 3;
public abstract String getEx1( );
String getEx2( ); // public abstract String getEx2( );
}
인터페이스 상속
인터페이스는 인터페이스로부터만 상속을 받을 수 있으며, 클래스와는 달리 다중 상속이 가능하다. 즉, 여러 개의 인터페이스로 부터 상속을 받는 것이 가능하다.
예를 들어 정의된 멤버가 하나도 없는 Z라는 자손 인터페이스가 있다고 했을 때, 이 자손 인터페이스는 조상 인터페이스인 A와 B로부터 다중 상속을 받을 수 있다.
interface Z extends A, B{ }
이때 A에는 move(int x, int y)라는 추상 메소드가 있었고, B에는 attack(Unint u)라는 추상 메소드가 있었다면 Z는 이 추상 메소드 2개를 모두 멤버로 가지게 된다.
인터페이스 구현
인터페이스도 추상 클래스처럼 자신에게 정의된 추상 메소드의 몸통을 만들어주는 클래스를 작성해야 하는데, 그 방법은 추상 클래스가 자신을 상속받는 클래스를 정의하는 방식과 같다. 다만 키워드를 extends 대신 implements를 사용한다.(인터페이스 간의 상속에는 extends 사용)
만약 상속 받은 클래스가 인터페이스의 메소드 중 일부만 구현할 것이라면, 클래스 앞에 abstract를 붙여서 추상 클래스로 선언해야 한다. 또한, 상속과 구현을 동시에 할 수도 있다.( class X extends Q implements W{ } )
Fighter 클래스는 Unit 클래스로부터 상속 받고, Fightable 인터페이스만 구현했지만, Fightable는 Moveable 인터페이스와 Attackable인터페이스의 자손이기 때문에 이 모든 클래스와 인터페이스의 자손 클래스라고 할 수 있다.
인터페이스를 이용한 다중상속
Tv클래스와 VCR클래스를 상속받는 TVCR클래스를 만들고 싶다고 할 때 클래스는 다중상속이 불가능하기 때문에 한 쪽은 상속을 받고, 한 쪽은 인터페이스로 만들어 클래스 내에서 인스턴스를 생성하고, 내용을 구현하도록 인터페이스를 사용할 수 있다.
Tv클래스를 상속 받았다면, VCR클래스에 정의된 메소드와 일치하는 추상 메소드를 가지는 IVCR 인터페이스를 작성하고 이를 TVCR 클래스에서 구현한다.
TVCR클래스에서 VCR 클래스 타입의 참조변수를 멤버변수로 선언해서 IVCR 인터페이스의 추상 메소드를 구현하는데 사용하면 된다.
public class TVCR extends Tv implements IVCR{
VCR vcr = new VCR( ); // VCR인스턴스 생성
public void play( ){
vcr.play( ); // VCR 클래스 타입의 참조변수를 멤버변수로 선언해서 IVCR 인터페이스 추상메소드 구현
}
}
인터페이스를 이용한 다형성 - 예제 어렵다 나중에 다시 보기
인터페이스는 메소드의 매개변수의 타이으로 사용될 수 있다.
void attack(Fightable f){ }
인터페이스 타입의 매개변수가 갖는 의미는 메소드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야한다는 것이다.
즉, Fightable 인터페이스를 구현한 클래스는 Fighter클래스로 attack메소드를 호출할 때는 Fighter의 인스턴스를 넘겨주어야 한다는 것이다.
또한 메소드의 리턴 타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.
Fightable method( ) {
Fighter f = new Fighter( );
return f;
}
위와 같이 리턴타입이 인터페이스라는 것은 메소드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.
위의 코드에서는 method( )의 리턴 타입이 Fightable 인터페이스 이기 때문에 메소드의 return에서 Fightable 인터페이스를 구현한 Fighter클래스의 인스턴스를 반환한다.
인터페이스의 장점
- 개발시간 단축 가능
- 표준화 가능
- 서로 관계없는 클래스들에게 관계를 맺어줄 수 있음
- 독립적인 프로그래밍이 가능
Unit - GroundUnit, AirUnit
GroundUnit - Marine, SCV, Tank
AirUnit - Droship
위와 같은 관계도가 있을 때, SCV, Tank, Dropship을 수리할 수 있는 기능을 제공하고자 repair 메소드를 생성하고자 할 때 인터페이스를 사용하면 기존의 상속체계를 유지하면서 이들 유닛에 공통점을 부여할 수 있다.
아래와 같이 Repairable 인터페이스를 정의하고 수리가 가능한 유닛에게 이 인터페이스를 구현하도록 하면 된다.
interface Repairable{ }
class SCV extends GroundUnit implements Repairable { 구현 }
class Tank extends GroundUnit implements Repairable { 구현 }
class Droship extends AirUnit implements Repairable { 구현 }
위의 3개의 클래스에는 이제 같은 인터페이스를 구현했다는 공통점이 생겼다. 그리고 repair메소드의 매개변수 타입을 Repairable로 선언하면, 이 메소드의 매개변수로 Repairable인터페이스를 구현한 클래스의 인스턴스만 받아들여 질 것이다.
void repair(Repairable r){
매개변수로 넘겨받은 유닛 수리
}
이제 새로운 클래스가 추가될 때마다, SCV의 repair 메소드에 의해서 수리가 가능하도록 하려면 그 클래스가 Repairable인터페이스를 구현하도록 하면 된다.
*예제 나중에 추가
인터페이스의 이해
인터페이스를 이해하기 위해서는 두 가지 사항을 반드시 염두에 두고 있어야 한다.
- 클래스를 사용하는 쪽(User)과 클래스를 제공하는 쪽(Provider)이 있다.
- 메소드를 사용(호출)하는 쪽(User)에서는 사용하려는 메소드(Provider)의 선언부만 알면 된다.
위의 예제에서 User는 클래스 A이고, Provider은 클래스 B다. 클래스 A는 클래스 B의 인스턴스를 생성하고 메소드를 호출한다. 이 두 클래스는 서로 직접적인 관계에 있음을 알 수 있다.
위의 경우에서는 클래스 A를 작성하기 위해서는 클래스 B가 생성되어 있어야 하며, 클래스 B의 methodB( )의 선언부가 변경되면, 이를 사용하는 클래스 A도 변경되어야 한다.
이처럼 직접적인 관계의 두 클래스는 Provider이 변경되면 User도 변경되어야 한다는 번거로움이 존재한다.
하지만 클래스 A가 클래스 B를 직접적으로 호출하지 않고, 인터페이스를 매개체로 클래스 A가 클래스 B의 메소드에 접근하도록 하면, 클래스 B에 변경사항이 생기거나 클래스 B와 같은 기능을 가진 다른 클래스로 대체되어도 클래스 A는 전혀 영향을 받지 않는다.
이렇게 두 클래스간의 관계를 간접적으로 변경하기 위해서는 먼저 인터페이스를 사용해서 클래스 B(Provider)의 선언과 구현을 분리해야 한다.
위의 예제처럼 인터페이스를 사용하면 A-B의 직접적인 관계에서 A-I-B의 간접적인 관계로 클래스간의 관계가 변하는것을 알 수 있다.
클래스 A는 여전히 클래스 B의 메소드를 호출하지만, 클래스 A는 인터페이스 I하고만 직접적인 관계에 있기 때문에 클래스 B의 변경에 영향을 받지 않는다.
클래스 A가 인터페이스 I를 사용해서 작성되기는 했지만, 위의 예제에서 볼 수 있듯이 매개변수를 통해서 인터페이스 I를 구현한 클래스의 인스턴스를 동적으로 제공받아야 한다.
위처럼 매개변수를 통해서 동적으로 제공받을 수도 있지만 제 3의 클래스를 통해서 제공받을 수도 있다.
위의 예제는 인스턴스를 직접 생성하지 않고, getInstance( ) 메소드를 통해서 제공받는다. 이런 방식으로 코드를 짜면 나중에 다른 클래스의 인스턴스로 변경되어도 A 클래스의 변경없이 getInstance( ) 메소드만 변경하면 된다는 장점이 있다.
디폴트 메소드
일반 조상 클래스와 달리 인터페이스에서 새로운 메소드의 추가는 굉장히 큰 일이다. 왜냐하면 인터페이스에 새로운 메소드를 추가한다는 것은 추상 메소드를 추가한다는 것이고, 이는 이 인터페이스를 구현한 기존의 모든 클래스가 새로 추가된 메소드를 구현해야 한다는 뜻이기 때문이다.
그래서 등장한 것이 바로 디폴트(default) 메소드다. 디폴트 메소드는 추상 메소드가 아니기 때문에 새로 추가되어도 해당 인터페이스를 구현한 클래스를 변경하지 않아도 된다.
디폴트 메소드는 앞에 키워드 default를 붙이며, 추상 메소드와는 달리 일반 메소드 처럼 중괄호 안의 몸통이 있어야 한다.