추상 클래스는 그 자체로 인스턴스가 될 수 없으며, 상속받는 자손이 인스턴스가 된다. 그리고 abstract 키워드를 사용하여 클래스를 정의한다.
추상 클래스는 보통 1개 이상의 추상 메서드를 가지지만, 없다해도 오류가 발생하진 않는다.
추상 메서드
다음의 Car2 클래스를 만든 사람이 run()이라는 메서드가 필요하다라고 생각을 하고, run()은 자동차마다 다르게 구현할 것 같다고 생각이 들면 추상메서드를 만들게 된다.
public abstract class Car2 {
public Car2(String name) {
System.out.println("Car2() 생성자 호출");
}
// 추상메서드
public abstract void run();
}
그리고 부모가 가지는 추상메서드는 자식 클래스에서 반드시 Overriding하여 구현해줘야 한다.
public class Bus2 extends Car2{
public Bus2() {
super("I am Bus");
System.out.println("Bus2 기본 생성자 호출");
}
@Override
public void run() {
System.out.println("후륜구동으로 동작한다.");
}
}
public class Car2Main {
public static void main(String[] args) {
Bus2 b = new Bus2();
b.run();
}
}
이를 main 메서드에서 실행하면, 다음과 같은 결과를 얻을 수 있다.
Car2() 생성자 호출
Bus2 기본 생성자 호출
후륜구동으로 동작한다.
이때, 아래와 같이 추상 클래스로 선언된 참조타입을 가진 참조변수가 있으면, 컴파일러는 실행되기 전까지 인스턴스를 생성하기 전이므로 어떤 인스턴스를 참조하는지 알 수 없는 상황에서는 c.run() 메서드가 어떤 결과를 출력할지 알 수 없다.
Car2 c = ...;
c.run();
즉, ... 부분이 어떤 인스턴스인지에 따라서 run메서드의 결과가 달라진다.
여기서 자바처럼 실행상황에서만 결과를 확인할 수 있는 언어를 동적인 언어라고 한다.
다음은 Car2를 2개 참조할 수 있는 배열을 선언하여, 각 배열에 인스턴스를 할당해주고 Car2 참조 변수로 참조하여 run 메서드를 실행시킨 예제이다.
Car2[] arr = new Car2[2];
arr[0] = new Bus2();
arr[1] = new SportsCar();
for (Car2 car2 : arr) {
car2.run();
}
Car2() 생성자 호출
Bus2 기본 생성자 호출
Car2() 생성자 호출
후륜구동으로 동작한다.
사륜구동으로 동작한다.
Template Method Pattern으로 배우는 추상 클래스
템플릿 메서드 패턴은 추상 클래스에서 가장 많이 사용되는 듯하다. 여기서 내가 가지고 있는 메서드를 호출하는데 어떤 순서를 가지고 있는 메서드를 템플릿 메서드라고 한다. 코드로 보면 아래의 execute 메서드가 템플릿 메서드이다.
/**
* Controller의 종류가 여러 개이고, 초기화 부분과 마무리 부분의 코드는 같고, 실행 부분만 코드가 다르다.
*/
public abstract class Controller {
public void init() {
System.out.println("초기화 코드");
}
public void close() {
System.out.println("마무리 코드");
}
// 실행 코드
public abstract void run();
// 템플릿 메서드
public void execute() {
init();
run();
close();
}
}
import theory.framework.Controller;
public class FirstController extends Controller {
@Override
public void run() {
System.out.println("별도로 동작하는 코드 - 1");
}
}
import theory.framework.Controller;
import theory.myproject.FirstController;
public class ControllerMain {
public static void main(String[] args) {
Controller c = new FirstController();
c.execute();
}
}
위와같이 Controller를 상속받게 함으로써 매번 구현하는 부분이 달라지게 하고, execute()만 호출하면 init() -> run() -> close() 부분이 실행되도록 해줄 수 있다.
초기화 코드
별도로 동작하는 코드 - 1
마무리 코드
그리고 추가적으로 사용자가 execute()의 순서로만 실행되게 만들어주기 위해서 init(), close()는 따로 따로 호출되도록 해주면 안된다. 이때, 접근제한자 proetected가 역할을 한다.
- protected는 같은 package이거나 상속받았을 경우에 접근이 가능하다.
public abstract class Controller {
protected void init() {
System.out.println("초기화 코드");
}
protected void close() {
System.out.println("마무리 코드");
}
// 실행 코드
protected abstract void run();
// 템플릿 메서드
public void execute() {
init();
run();
close();
}
}
하지만 이때 또 문제가 발생할 수 있다. 바로 자식 클래스에서 init() 메서드나 close() Overriding하여 내용을 바꿀 수 있다는 점이다. 이때 메서드 final 을 붙이면 abstract와 반대의 기능. 즉, Overriding을 금지하는 기능이 추가된다.
public abstract class Controller {
protected final void init() {
System.out.println("초기화 코드");
}
protected final void close() {
System.out.println("마무리 코드");
}
// 실행 코드
protected abstract void run();
// 템플릿 메서드
public void execute() {
init();
run();
close();
}
}
인용
https://youtu.be/-LZIyblqyWs?si=NBe5U4xDFTmjD7_A
'Language > Java' 카테고리의 다른 글
인터페이스 (0) | 2023.09.15 |
---|---|
부모가 될 수 없는 클래스 - String (0) | 2023.09.15 |
super() 생성자와 불변 객체 (0) | 2023.09.14 |
상속 (0) | 2023.09.14 |
Package (0) | 2023.09.13 |