지금까지 다뤄왔던 클래스는 concrete class로 추상 클래스인 abstract class 와 상반된다. 추상적인 클래스란 구체적이지 않은 클래스, 완전하지 않은 클래스를 의미하며, 실제로 추상 클래스의 구현은 메소드를 구현하지 않고 선언만 해두는 추상 메서드를 포함함으로써 이루어진다.
public abstract class A {
}
만약 추상 클래스를 만들고 싶다면 class 앞에 abstract
예약어를 붙이면 된다. (추상 메서드도 마찬가지로 abstract
예약어를 사용한다.)
추상 메서드는 구현부가 존재하지 않는 메서드이다. 구현부는 함수의 몸체라고도 표현하며, 함수의 구현부는 중괄호{ }
로 감싼 모든 부분이다. 즉, 추상메서드는 { }
없이 abstract
예약어와 함께 선언만하는 메서드인 것이다.
abstract int add(int x, int y);
추상 메서드의 선언은 abstract
예약어, 메서드의 반환값 타입, 메서드 이름, 메서드의 매개변수 개수와 타입으로 이루어진다.
여기서 주의해야할 점은 { }
안이 비었더라도, { }
를 사용하는 순간 함수를 구현한 것이기 때문에 { }
가 존재하면 추상 메서드가 아니라는 것이다. 바로 아래와 같은 경우는 추상 메서드가 아니다.
/* 구현부({ })가 존재하므로 추상 클래스가 아님 */
int add(int x, int y) { }
클래스 내부에 추상 메서드가 하나라도 존재한다면, 그 클래스는 추상 클래스가 되어야한다. 만약 추상 클래스로 사용하고 싶지 않다면, 그 메서드를 구현해주어야 오류없이 구현할 수 있다. 즉, concrete 클래스는 추상 메서드를 포함할 수 없다.
반면, abstract
예약어만 적어준다면, 추상 메서드가 아닌 메서드만으로 이루어진 추상 클래스는 만들 수 있다. 하지만 일반적으로 추상 클래스는 추상메서드를 포함하고 있다.
package abstractex;
public abstract class Computer {
/* 선언만 해두는 추상 메서드 */
public abstract void display();
public abstract void typing();
public Computer() {
System.out.println("Computer Constructor!");
}
public void turnOn() {
System.out.println("Computer turnOn()");
}
public void turnOff() {
System.out.println("Computer turnOff()");
}
}
추상 클래스는 구현이 이루어지지 않은 메소드를 포함하고 있으므로 추상클래스는 단독으로 인스턴스를 생성할 수가 없다.
Computer myComputer = new Computer();
이렇게 new
예약어와 생성자를 사용하여 추상 클래스의 인스턴스를 생성하고자하면 오류가 날 것이다. 구현한 추상 클래스를 사용하고 싶다면, 추상 클래스를 상속받은 하위클래스가 모든 추상 메서드를 오버라이딩해서 구현해주어야한다. 즉, 추상 클래스를 사용한다면 상속은 필수 불가결한 요소라는 것이다.
만약 추상 메서드가 3개인데, 하위 클래스에서 1개만 구현하고 싶다면 그렇게 할 수 있다. 다만 이런 상황이라면 이 하위 클래스도 추상 클래스로 선언해야한다. 그리고 이 클래스를 상속받은 하위 클래스에서 나머지 추상 메서드를 구현하기만 한다면 추상 클래스를 사용할 수 있다.
package abstractex;
public class DeskTop extends Computer {
public DeskTop() {
System.out.println("DeskTop Constructor!");
}
@Override
public void display() {
System.out.println("DeskTop display()");
}
@Override
public void typing() {
System.out.println("DeskTop typing()");
}
}
package abstractex;
public abstract class NoteBook extends Computer {
public NoteBook() {
System.out.println("NoteBook Constructor!");
}
@Override
public void display() {
System.out.println("NoteBook display()");
}
}
package abstractex;
public class MacBook extends NoteBook {
public MacBook() {
System.out.println("MacBook Constructor!");
}
@Override
public void typing() {
System.out.println("Macbook typing()");
}
}
package abstractex;
public class ComputerTest {
public static void main(String[] args) {
System.out.println("===== Create DeskTop =====");
Computer c1 = new DeskTop(); // 업캐스팅
System.out.println("\n===== Create MackBook =====");
Computer c2 = new MacBook(); // 업캐스팅
/* 추상 클래스는 인스턴스로 생성할 수 없다. */
// Computer c3 = new Computer();
// Computer c4 = new NoteBook();
}
}
===== Create DeskTop =====
Computer Constructor!
DeskTop Constructor!
===== Create MackBook =====
Computer Constructor!
NoteBook Constructor!
MacBook Constructor!
추상 클래스가 아닌 클래스 간의 상속과 마찬가지로 하위 클래스 생성자를 호출하면 자동으로 추상클래스의 여부와 상관 없이 상위 클래스의 생성자가 호출된다. 추상 클래스는 인스턴스를 생성할 수는 없지만 하위 클래스의 생성자를 통해 생성자를 사용할 수 있으며, 생성자 오버로딩도 가능하다.
그리고 상위 클래스가 추상 메서드를 포함하고 있을 뿐 하위 클래스가 상위 클래스를 내포하고 있다는 사실은 변함이 없으므로 추상 클래스와 일반 클래스 간의 상속 관계에서도 업캐스팅이 가능하다.
추상 클래스는 상속을 위해 만드는 클래스다.
추상 클래스의 메서드는 크게 추상 메서드와 추상 메서드가 아닌 메서드로 나뉘는데, 다시 말하면 선언만 한 메서드와 구현된 메서드로 나뉜다는 것이다.
어떤 메서드를 추상 메서드로 선언할지는 하위 클래스에서 메서드를 사용하는 방식에 따라 결정된다. 하위 클래스에서 공통적으로 사용할 기능이라면 추상 클래스에서 구현하지만, 하위 클래스마다 기능이 달라야 한다면 추상 메서드로 남겨두는 것이다.
ComputerTest
package abstractex;
public class ComputerTest {
public static void main(String[] args) {
Computer desktop = new DeskTop();
Computer macbook = new MacBook();
System.out.println("===== DeskTop =====");
desktop.turnOn();
desktop.display();
desktop.typing();
desktop.turnOff();
System.out.println("\n===== MackBook =====");
macbook.display();
macbook.turnOn();
macbook.typing();
macbook.turnOn();
}
}
===== DeskTop =====
Computer turnOn()
DeskTop display()
DeskTop typing()
Computer turnOff()
===== MackBook =====
NoteBook display()
Computer turnOn()
Macbook typing()
Computer turnOn()
모든 클래스가 공통으로 사용하는 turnOn
과 turnOff
메서드는 추상 클래스에서 구현했지만, display
와 typing
메서드는 추상 메서드로 남겨두고, 구현을 하위 클래스에게 위임하고 있다.
물론 turnOn
과 turnOff
처럼 구현된 메서드도 하위 클래스에서 기능을 다르게 하고 싶다면, 오버라이딩을 하면 된다.
선언부만 존재하는 추상 메서드는 중요한 의미를 갖는다.
사실 함수의 논리 흐름을 구현하는 것보다 중요한 것은 함수 선언부를 작성하는 것이라고 할 수 있다. 메서드를 선언한다는 것은 함수의 기능을 정의하고 어떻게 구현할지 결정하는 것이고, 이 과정을 개발 설계라고 한다.
int add(int num1, int num2);
이처럼 메서드의 선언부는 메서드의 이름, 메서드의 반환값 타입, 메서드의 매개변수 반환값 타입과 개수로 이루어져 있는데, 보통 선언부만 보아도 함수가 어떤 역할을 하는지 알 수 있다. 예를 들어, add
함수는 num1
와 num2
를 더한 값을 반환해주는 함수임을 선언부를 통해 추측가능하다.
추상 클래스를 만든다는 것은 상속 관계의 클래스를 설계하는 일이라고 볼 수도 있다.
템플릿 메서드는 싱글톤 패턴처럼 일종의 디자인 패턴이다.
템플릿(template) 이라는 단어에는 틀, 견본과 같은 뜻이 있다. 템플릿 메소드도 어떤 틀과 같은 역할을 하는 메서드라고 할 수 있다. 추상 클래스 내의 메서드 실행 순서와 시나리오를 정의하는 메서드이기 때문이다.
Computer
추상 클래스public abstract class Computer {
public abstract void display();
public abstract void typing();
public Computer() {
System.out.println("Computer Constructor!");
}
public void turnOn() {
System.out.println("Computer turnOn()");
}
public void turnOff() {
System.out.println("Computer turnOff()");
}
/* 템플릿 메서드 */
final public void run() {
turnOn();
display();
typing();
turnOff();
}
}
run()
메서드는 컴퓨터가 실행되면 동작하는 순서를 정의해둔 것이다. 먼저 전원을 켜고(turnOn), 화면이 켜지면(display), 타자를 치고(typing), 전원을 끈다(turnOff).
이 순서는 Computer
클래스를 상속받은 클래스라면 모두 동일하며, 바뀌어서도 안된다. 템플릿 메서드는 로직을 정의한 메소드이기 때문이다. 그래서 final
예약어를 사용하여 하위 클래서에서의 오버라이딩을 막는 것이다.
public class ComputerTest {
public static void main(String[] args) {
Computer desktop = new DeskTop();
Computer macbook = new MacBook();
System.out.println("===== DeskTop =====");
desktop.run();
System.out.println("\n===== MackBook =====");
macbook.run();
}
}
===== DeskTop =====
Computer turnOn()
DeskTop display()
DeskTop typing()
Computer turnOff()
===== MackBook =====
Computer turnOn()
NoteBook display()
Macbook typing()
Computer turnOff()
final
final
예약어는 말 그대로 마지막으로 정했으니 더이상 수정할 수 없다는 의미를 갖고 있다.
final
예약어는 변수, 메서드, 클래스에 사용할 수 있으며, 변수 앞에 사용하면 상수가 되고, 메서드 앞에 사용하면 오버라이딩이 불가능하다. 그리고 클래스 앞에 사용하면 그 클래스는 상속할 수 없는 클래스가 된다.
추상클래스 개념 정리 잘하셨네요. 글 잘 보고 갑니다~