Java

[Java] 추상클래스와 인터페이스

hh_lin 2019. 1. 3. 22:00

추상클래스

  • 미완성 클래스로 추상메서드를 포함하고 있다는 것을 제외하고는 일반 클래스와 전혀 다르지 않다.
  • 생성자, 멤버변수, 메서드를 가진다.
  • 인스턴스를 생성할 수 없고, 자손클래스에 의해 완성될 수 있다.
  • 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스로서 중요한 의미를 가진다.
  • 키워드 'abstract'를 붙이면 추상클래스로 표현된다.

추상메서드

선언부만 작성하고 구현부는 작성하지 않은 채로 남겨둔 것.

즉, 설계만 해놓고 실제 수행될 내용은 작성하지 않았기 때문에 미완성 메서드.

abstract 리턴타입 메서드이름();

'abstract' 키워드를 붙이고, 구현부가 없으므로 괄호{ } 대신 문장의 끝을 알리는 ';'을 적어줌.


메서드를 미완성 상태로 남겨놓는 이유는 

메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언부만을 작성하고, 

주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워두는 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
abstract class Player {
    abstract void play(int pos);
    abstract void stop();
}
 
class AudioPlayer extends Player {
    void play(int pos) {
        // ... 
    }
    void stop() {
        // ...
    }
}
cs


추상클래스로부터 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상메서드를 모두 구현해주어야 한다.

상속받은 추상메서드 중 하나라도 구현하지 않는다면, 자손클래스 역시 추상클래스로 지정해주어야 한다.

1
2
3
4
5
6
7
8
abstract class AbstractPlayer extends Player {
    void play(int pos) {
        // ...
    }
}
 
abstract class AbstractPlayer2 extends Player {
}
cs


다음과 같이 아무 내용도 없는 메서드로 작성해도 자손 클래스에서 필요한 경우 오버라이딩을 통해 작성할 수 있지만, 

추상메서드로 선언함으로써 자손클래스에서 추상메서드를 반드시 구현하도록 강요할 수 있다.

1
2
void play(int pos) {}
abstract void play(int pos);
cs




추상화 vs 구체화

추상화 - 클래스간의 공통점을 찾아내서 공통의 조상을 만드는 작업

구체화 - 상속을 통해 클래스를 구현, 확장하는 작업




인터페이스

일종의 추상클래스로 추상메서드를 갖지만 추상클래스와 달리 몸통을 갖춘 일반 메서드 또는 멤버변수를 구성원으로 가질 수 없다.

오직, 추상메서드와 상수만을 멤버로 가질 수 있으며, 접근제어자로 public 또는 default를 사용할 수 있다.

1
2
3
4
interface 인터페이스이름 {
    public static final 타입 상수이름 = 값;
    public abstract 메서드이름(매개변수목록);
}
cs




인터페이스의 제약조건

- 모든 멤버변수는 public static final 이어야 하며, 이를 생략할 수 있다.

- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다. (단, static 메서드와 default 메서드는 예외)


1
2
3
4
5
6
7
8
9
interface PlayingCard {
    public static final int SPADE = 4;
    final int DIAMOND = 3;        // public static final int DIAMOND = 3;
    static int HEART = 2;         // public static final int HEART = 2;
    int CLOVER = 1;               // public static final int CLOVER = 1;
 
    public abstract String getCardNumber();
    String getCardKind();         // public abstract String getCardKind();
}
cs


  • 인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 제어자를 생략할 수 있는 것이며, 편의상 생략하는 경우가 많다.
  • 생략된 제어자는 컴파일 시에 컴파일러가 자동적으로 추가해준다.
  • 인터페이스의 모든 메서드는 추상메서드이어야 하는데, JDK 1.8부터 인터페이스에 static 메서드와 default 메서드의 추가를 허용하는 방향으로 변경되었다.


인터페이스의 상속

인터페이스는 인터페이스로부터만 상속받을 수 있으며, 클래스와는 달리 다중상속이 가능하다.

클래스의 상속과 마찬가지로 자손 인터페이스는 조상 인터페이스에 정의된 멤버를 모두 상속받는다.

1
2
3
4
interface Upper1 {}
interface Upper2 {}
 
interface Lower extends Upper1, Upper2 {}
cs




인터페이스의 구현

1
2
3
4
5
6
7
8
9
class 클래스이름 implements 인터페이스이름 {
    // 인터페이스에 정의된 추상메서드를 구현해야 한다.
}
 
abstract class 클래스이름 implements 인터페이스이름 {
}
 
class LowerClass extends UpperClass implements UpperInterface {
}
cs

  • 추상클래스와 마찬가지로 그 자체로는 인스턴스를 생성할 수 없다.
  • 추상클래스가 상속을 통해 추상메서드를 완성하는 것처럼, 인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야 한다.
  • 클래스와는 다르게 구현한다는 의미의 키워드 'implements'를 사용한다.
  • 만일 구현하는 인터페이스의 메서드 중 일부만 구현한다면, abstract를 붙여서 추상클래스로 선언해야 한다.
  • 상속과 구현을 동시에 할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Main {
 
    public static void main(String[] args) {
        Fighter f = new Fighter();
        
        if(f instanceof Unit) 
            System.out.println("f는 Unit 클래스의 자손입니다.");
        
        if(f instanceof Fightable) 
            System.out.println("f는 Fightable 인터페이스를 구현했습니다.");
        
        if(f instanceof Movable) 
            System.out.println("f는 Movable 인터페이스를 구현했습니다.");
        
        if(f instanceof Attackable) 
            System.out.println("f는 Attackable 인터페이스를 구현했습니다.");
        
        if(f instanceof Object) 
            System.out.println("f는 Object 클래스의 자손입니다.");
    }
 
}
 
class Fighter extends Unit implements Fightable {
    public void move(int x, int y) {}
    public void attack(Unit u) {}
}
 
class Unit {
    int currentHP;
    int x;
    int y;
}
 
interface Fightable extends Movable, Attackable {}
interface Movable { void move(int x, int y); }
interface Attackable { void attack(Unit u); }
cs



Movable 인터페이스에 정의된 move 메서드를 Fighter 클래스에서 구현할 때 접근제어자를 public으로 한 이유는

오버라이딩할 때는 조상의 메서드보다 넓은 범위의 접근제어자를 지정해야 하기 때문이다.

Movable 인터페이스의 move메서드는 실제 public abstract void move(int x, int y)이기 때문에 이를 구현하는 Fighter 클래스에서는 move 메서드의 접근 제어자를 반드시 public으로 해야 한다.




인터페이스를 이용한 다중상속

인터페이스를 이용하면 C++과 마찬가지로 다중상속이 가능하지만, 실제로 자바에서 인터페이스로 다중상속을 구현하는 경우는 거의 없다.

인터페이스는 static 상수만 정의할 수 있으므로 조상클래스의 멤버변수와 충돌하는 경우는 거의 없고 충돌된다 하더라도 클래스 이름을 붙여서 구분이 가능하다. 뿐만 아니라 추상메서드는 구현 내용이 전혀 없기 때문에 조상클래스의 메서드와 선언부가 일치하는 경우에는 당연히 조상클래스 쪽의 메서드를 상속받으면 되므로 문제되지 않는다.

그러나 이렇게 하면 상속받는 멤버의 충돌은 피할 수 있지만, 다중상속의 장점을 잃게 된다.

두 조상클래스 중에서 비중이 높은 쪽을 선택하고 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하거나 어느 한쪽의 필요한 부분을 뽑아서 인터페이스로 만든 다음 구현하도록 한다.




인터페이스를 이용한 다형성

자손클래스의 인스턴스를 조상타입의 참조변수로 참조하는 것이 가능한 것처럼 인터페이스 역시 해당 인터페이스 타입의 참조변수로 이를 구현한 클래스의 인스턴스를 참조할 수 있으며, 인터페이스 타입으로의 형변환도 가능하다.

ex) 인터페이스 Fightable을 클래스 Fighter가 구현했을 때, 다음과 같이 Fighter 인스턴스를 Fightable 타입의 참조변수로 참조하는 것이 가능하다.

1
2
3
Fightable f = (Fightable) new Fighter();
 
Fightable f = new Fighter();
cs


뿐만 아니라, 인터페이스는 메서드의 매개변수 타입으로 사용될 수 있다. 

인터페이스 타입의 매개변수가 갖는 의미는 메서드 호출 시 해당 인터페이스를 구현한 클래스의 인스턴스를 매개변수로 제공해야 한다는 것이다.

1
2
3
4
void attack(Fightable f) {
}

attack(new Fighter());
cs


메서드의 리턴타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다.

리턴타입이 인터페이스라는 것은 메서드가 해당 인터페이스를 구현한 클래스의 인스턴스를 반환한다는 것을 의미한다.

1
2
3
4
Fightable method() {
    // ...
    return new Fighter();
}
cs




인터페이스의 장점

1. 개발시간을 단축시킬 수 있다.

- 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는 것이 가능하다. 

  메서드를 호출하는 쪽에서는 메서드의 내용에 관계없이 선언부만 알면 되기 때문이다.


2. 표준화가 가능하다.

- 프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 

  인터페이스를 구현하여 프로그램을 작성하게 함으로써 보다 일관되고 정형화된 프로그램 개발이 가능하다.


3. 서로 관계없는 클래스들에게 관계를 맺어줄 수 있다.

- 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있다.


4. 독립적인 프로그래밍이 가능하다.

- 인터페이스를 이용하면 클래스의 선언과 구현을 분리시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다. 

  클래스간의 직접적인 관계를 인터페이스를 이용해서 간접적인 관계로 변경하면, 한 클래스의 변경이 관련된 다른 클래스에 영향을 

  미치지 않는 독립적인 프로그래밍이 가능하다.







참고자료

자바의 정석