추상클래스란?
클래스를 설계도에 비유한다면, 추상클래스는 미완성 설계도에 비유할 수 있다.
클래스가 미완성이라는 것은 멤버의 개수에 관계된 것이 아니라, 단지 미완성 메서드(추상 메서드)를 포함하고 있다는 의미이다.
미완성 설계도로 완성된 제품을 만들 수 없듯이 추상 클래스로 인스턴스는 생성할 수 없다. 추상 클래스는 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.
추상클래스 자체로는 클래스로서의 역할을 다 못하지만, 새로운 클래스를 작성하는 데 있어서 바탕이 되는 조상 클래스로서 중요한 의미를 갖는다. 새로운 클래스를 작성할 때 아무것도 없는 상태에 시작하는 것보다는 완전하지는 못하더라도 어느 정도 틀을 갖춘 상태에서 시작하는 것이 나을 것이다.
문법
추상 클래스는 키워드 'abstract'를 붙이기만 하면 된다. 이렇게 함으로써 이 클래스를 사용할 때, 클래스 선언부의 abstract를 보고 이 클래스에는 추상 메서드가 있으니 상속을 통해서 구현해주어야 한다는 것을 쉽게 알 수 있다.
abstract class 클래스이름 {
...
}
추상 클래스는 추상 메서드를 포함하고 있다는 것을 제외하고는 일반 클래스와 전혀 다르지 않다. 추상 클래스에도 생성자가 있으며, 멤버 변수와 메서드도 가질 수 있다.
추상 메서드를 포함하고 있지 않은 클래스에도 abstract를 붙여서 추상 클래스로 지정할 수도 있다. 추상 메서드가 없는 완성된 클래스라 할지라도 추상클래스로 지정되면 클래스의 인스턴스를 생성할 수 없다.
추상 메서드(abstract method)
선언 부만 작성하고 구현부는 작성하지 않은 것이 추상 메서드이다.
메서드를 이와 같이 미완성 상태로 남겨 놓는 이유는 메서드의 내용이 상속받는 클래스에 따라 달라질 수 있기 때문에 조상 클래스에서는 선언 부만을 작성하고, 주석을 덧붙여 어떤 기능을 수행할 목적으로 작성되었는지 알려 주고, 실제 내용은 상속받는 클래스에서 구현하도록 비워 두는 것이다. 그래서 추상 클래스를 상속받는 자손 클래스는 조상의 추상 메서드를 상황에 맞게 적절히 구현해주어야 한다.
문법
추상 메서드 역시 키워드 'abstract'를 앞에 붙여 주고, 추상 메서드는 구현부가 없으므로 괄호 {} 대신 ;을 적어준다.
/* 주석을 통해 어떤 기능을 수행할 목적으로 작성하였는지 설명한다. */
abstract 리턴타임 메서드이름();
추상 클래스로부터 상속받는 자손클래스는 오버라이딩을 통해 조상인 추상클래스의 추상 메서드를 모두 구현해주어야 한다. 만일 조상으로부터 상속받은 추상메서드 중 하나라도 구현하지 않는다면, 자손클래스 역시 추상클래스로 지정해 주어야 한다.
abstract class Player {
abstract void play(int pos); // 추상 메서드
abstract void stop(); // 추상 메서드
}
class AudioPlayer extends Player {
void play(int pos) { ... } // 추상 메서드 구현
void stop() { ... } // 추상 메서드 구현
}
abstract class AbstractPlayer extends Player {
void play(int pos) { ... } // 추상 메서드 구현
}
메서드를 작성할 때 구현 부보다 더 중요한 부분이 선언 부이다.
메서드의 이름과 메서드의 작업에 필요한 매개변수, 그리고 작업의 결과로 어떤 타입의 값을 반환할 것인가를 결정하는 것은 쉽지 않은 일이다. 선언 부만 작성해도 메서드의 절반 이상이 완성된 것이라 해도 과언이 아니다.
메서드를 사용하는 쪽은 메서드가 실제로 어떻게 구현되어 있는지 몰라도 메서드의 이름과 매개변수, 리턴 타입, 즉 선언 부만 알고 있으면 되므로 내용이 없을지라도 추상 메서드를 사용하는 코드를 작성하는 것이 가능하며, 실제로는 자손 클래스에 구현된 완성된 메서드가 호출되도록 할 수 있다.
추상 클래스의 작성
여러 클래스에 공통적으로 사용될 수 있는 클래스를 바로 작성하기도 하고, 기존의 클래스의 공통적인 부분을 뽑아서 추상 클래스로 만들어 상속하도록 하는 경우도 있다.
상속이 자손 클래스를 만드는데 조상 클래스를 사용하는 것이라면, 이와 반대로 추상화는 기존의 클래스의 공통부분을 뽑아내서 조상 클래스를 만드는 것이라고 할 수 있다.
추상화를 구체화와 반대되는 의미로 이해하면 보다 쉽게 이해할 수 있다. 상속계층도를 따라 내려갈수록 클래스는 점점 기능이 추가되어 구체화의 정도가 심해지며, 그 반대는 추상화의 정도가 심해진다고 할 수 있다.
- 추상화 : 클래스 간의 공통점을 찾아내서 공통의 조상을 만드는 작업
- 구체화 : 상속을 통해 클래스를 구현, 확장하는 작업
abstract class Player {
boolean pause;
int currentPos;
Player() {
pause = false;
currentPos = 0;
}
abstract void play(int pos); // 추상 메서드
abstract void stop(); // 추상 메서드
void play() {
play(currentPos);
}
void pause() {
if (pause) {
pause = false;
play(currentPos);
} else {
pause = true;
stop();
}
}
}
class CDPlayer extends Player {
void play(int currentPos) {
// 조상의 추상 메서드 구현
}
void stop() {
// 조상의 추상 메서드 구현
}
int currentTrack;
void nextTrack() {
currentTrack++;
}
void preTrack() {
if (currentTrack > 1) {
currentTrack--;
}
}
}
사실 Player클래스의 play(int pos)와 stop()을 추상 메서드로 하는 대신, 아무 내용도 없는 메서드로 작성할 수도 있다. 아무런 내용도 없이 단지 괄호 { }만 있어도, 추상 메서드가 아닌 일반 메서드로 간주되기 때문이다.
class Player {
...
void play(int pos) { }
void stop() { }
...
}
어차피 자손 클래스에서 오버 라이딩하여 자신의 클래스에 맞게 구현할 테니 추상 메서드로 선언하는 것과 내용 없는 빈 몸통만 만들어 놓는 것이나 별 차이가 없어 보인다.
그래도 굳이 abstract를 붙여서 추상 메서드로 선언하는 이유는 자손 클래스에서 추상 메서드를 반드시 구현하도록 강요하기 위해서이다.
만일 추상 메서드로 정의되어 있지 않고 위와 같이 빈 몸통만 가지도록 정의되어 있다면,
상속받는 자손 클래스에서는 이 메서드들이 온전히 구현된 것으로 인식하고 오버라이딩을 통해 자신의 클래스에 맞도록 구현하지 않을 수도 있기 때문이다.
하지만, 추상 메서드로 정의해놓으면, 자손 클래스를 작성할 때 이들이 추상 메서드이므로 내용을 구현해주어야 한다는 사실을 인식하고 자신의 클래스에 알맞게 구현할 것이다.