본문 바로가기
JAVA

22. 인터페이스

by seongju.lee 2022. 11. 14.

인터페이스

  • 추상메서의 집합이라는 것이 핵심이자 본질이다.
  • 구현된 것이 없는 선언부만 존재한다.
  • 이름이 인터페이스인만큼 모든 멤버가 public이다.
  • 추상 클래스와의 차이?
    • 추상 클래스는 단지 "일반 클래스가 추상 메서드를 멤버로 가지고 있으면 추상 클래스"이다.
      (생성자, 멤버변수 등 다양하게 가질 수 있음)
    • 인터페이스는 "추상메서드의 집합"이다.

인터페이스 선언

interface PlayingCard{

    // 상수
    public static final int SPADE = 4;
    final int DIAMOND = 3;
    static int HEART = 2;
    int CLOVER = 1;

    // 추상 메서드
    public abstract String getCardNumber();
    String getCardKind();
}
  • 인터페이스에 변수는 올 수 없고, 무조건 상수만 올 수 있으므로
    public, static, final 등을 입력하지 않아도 자동으로 상수로 인지한다.
  • 인터페이스에 추상메서드만 올 수 있으므로,
    public, abstarct를 생략해도 자동으로 public 추상메서드 선언부로 인지한다.

 

인터페이스의 상속

  • 인터페이스의 조상은 인터페이스만 가능하다.
    (때문에 Object클래스는 인터페이스의 조상이 아니다.)
  • 다중 상속이 가능하다.
    • why?
      클래스의 다중상속이 안되는 근본적인 이유는 메서드의 충돌문제(메서드의 이름은 같고, 구현은 다를 수 있음)이다.
      그런데, 인터페이스의 경우에는 내부에 선언부만 존재하고, 구현부는 존재하지 않는다.
      때문에 메서드의 이름이 동일하다고 해도 전혀 문제가 되지 않기 때문이다.

인터페이스의 구현

  • 인터페이스에 정의되어 있는 추상 메서드를 구현하는 것이다.
interface Fightable{
    void move(int x, int y);
    void attack(Unit u);
}

class Fighter implements Fightable{

    public void move(int x, int y){
    	...
    }
    
    public void attack(Unit u){
    	....
    }
    
}
  • Fighter 클래스는 Fightable 인터페이스를 구현한 것이다.
  • 여기서 내가 놓쳤던 포인트는 인터페이스를 구현하는 메서드에서 메서드를 구현할 때는 public키워드를 붙어야 한다.
    왜냐하면 interface는 모든 메서드가 public 접근제어자인데, 하위 클래스는 상위 클래스의 접근 범위보다 좁아야 하기 때문이다.
  • 추가로, 만약 클래스에서 인터페이스의 일부 메서드만 구현한다면?
    • 추상클래스와 마찬가지로 abstract 키워드를 꼭 붙어줘야 한다.
      (모든 추상메서드를 구현한 것이 아니기 때문에 아직 추상클래스이다.)

인터페이스와 다형성

  • 인터페이스는 클래스의 다중상속이 불가능한 문제를 해결해주는 역할이 되기도 한다.
  • 근본적인 이유인 충돌문제를 해결했기 때문이다.
  • 그렇다면 구현 클래스의 부모 역할은 한다고 했을 때, 당연히 다형성이 가능하다.
class Fighter extends Unit implements Fightable{

    public void move(int x, int y){
    	...
    }
    
    public void attack(Fightable f){
    	...
    }
}

.
.

Unit u = new Fighter();
Fightable f = new Fighter();
  • 인터페이스와 클래스를 상속받은 Fighter 클래스가 있다.
  • 추상 클래스와 마찬가지로 맨 아래 코드처럼 인터페이스도 다형성이 가능하다.
    • Fightable 인터페이스로 자손 인스턴스를 참조하는 것이 가능.
    • 사용할 수 있는 멤버는 Fightable인터페이스에 선언된 추상 메서드만 사용 가능.
  • Fighter 클래스에서 attack 메서드를 구현한 것을 보아하니,
    Fightable인터페이스에서 attack메서드의 매개변수는 인터페이스 타입이었다는 것을 예측할 수 있는데, 여기서 중요한 것은
    매개변수 타입이 인터페이스라는 것은 들어올 수 있는 인자가 해당 인터페이스를 구현한 클래스의 인스턴스만 가능한 것이다.
    즉 위 예제에서는 f.attack(new Fighter()); 라는 코드로써 attack을 사용할 수 있는 것.

  • 인터페이스 다향성의 기나긴 예제
abstract class Unit{

    int x,y;
    abstract void move(int x, int y);
    void stop() {System.out.println("stop")}

}

interface Fightable{
    void move(int x, int y);
    void attack(Fightable f)
}

class Fighter extends Unit implements Fightable{

    public void move(int x, int y){
    	System.out.println(x+""+ "," + y+""+ "로 이동");
    }
    
    
    public void attack(Fightable f){
    	System.out.println(f+"를 공격");
    }

}

public class InterfaceEx {

    public static void main(String[] args) {
    
        Fighterable f = new Fighter();
        f.move(300,200);
        f.attack(new Fighter());
    
    }
}
  • main 메서드 내부에 3줄을 보고 다형성의 흐름을 이해할 수 있다.

인터페이스의 장점

인터페이스의 장점은 크게 두 가지가 있다.

  1. 의존적인 관계의 클래스들을 간접적인 관계로 변경할 수 있다.
  2. 서로 관계없는 클래스들을 관계 맺어줄 수 있다.
  3. (+ 표준화가 가능한 장점도 있다.)

1. 선언과 구현을 분리시킬 수 있다.

  • 선언과 구현을 분리시키는 인터페이스 덕분에 두 개의 클래스를 의존적인 관계에서 간접적인 관계로 바뀌어,
    코드가 유연해지고, 변경에 유리해진다.
  • 아래 두 가지 예를 살펴보자.

1-1. 직접적인 관계의 두 클래스(A - B)

class A {

    public void method(B b){
    	b.method();
    }
}

class B{

    public void method(){
    	System.out.println("class B");
    }
}

class C{

    public void method(){
    	System.out.println("class C");
    }
}

public class InterfaceEx{

    public static void main(){
    
        A a = new A();
        a.method(new B());
        
    }
}
  • 만약 위 코드처럼 A클래스가 B클래스에 의존적이라면,
    A클래스가 C클래스를 호출하고 싶을 때는 A클래스의 method(B b)를 method(C c)로 변경을 해줘야 한다.
  • 변경에 불리하다.

1-2. 간접적인 관계의 두 클래스(A - I - B)

class A {

    public void method(I i){
    	i.method();
    }
}

interface I {

    void method();
}

class B implements I {

    public void method(){
    	System.out.println("class B");
    }
}

class C implements I {

    public void method(){
    	System.out.println("class B");
    }
}



public class InterfaceEx{

    public static void main(){
    
        A a = new A(new B());
        a.method();
    }
}
  • A가 B에 접근하려고할 때, 가운데 인터페이스가 존재하는 느낌이다.
  • 위와 같이 인터페이스를 적용하여 선언부와 구현부를 나누어 준 덕분에,
    A가 C에 접근하고 싶을 때는 A 클래스를 수정할 필요가 없어지게 된다.
  • 변경에 유리한 유연한 설계가 가능하게 된다. 

 

2. 서로 관계없는 클래스들을 관계 맺어줄 수 있다.

  • 아래 이미지와 같은 상속계층도가 있다고 가정해보자.

  • 위 이미지에서 SCV, Tank, Dropship이라는 3개의 클래스간의 관계를 맺고 싶다면 어떻게 해야할까?
  • 만약 이 3개의 클래스들이 '수리'라는 작업을 받기 위해서 'repair'라는 클래스로 묶고싶다면?

 

  1. 오버로딩
void repair(Tank t){ ... }
void repair(SCV s){...}
void repair(Dropship d){...}
  • 위 처럼 오버로딩을 통해 구현할 수 있지만, 당연히 좀 아닌거같다는 생각이 든다..

 

   2.  인터페이스 사용

interface Repairable {}

class SCV extends GroundUnit implements Repairable{...}
class Tank extends GroundUnit implements Repairable{...}
class Dropship extends AirUnit implements Repairable{...}
  • 위 처럼 interface로 Repairable이라는 것을 만들고, 원하는 클래스마다 해당 인터페이스를 구현한 클래스로 만들어주면
    Repairable이라는 인터페이스 내에 선언된 추상 메서드를 구현할 수 있게 된다.

'JAVA' 카테고리의 다른 글

3. 예외와 예외처리  (0) 2022.12.15
1. 자바 컴파일 과정  (0) 2022.11.23
21. 추상 클래스, 추상 메서드  (0) 2022.11.11
20. 다형성의 장점  (0) 2022.11.10
19. 다형성, 참조변수의 형변환  (0) 2022.11.10