다형성과 추상 클래스
타입별 반복 코드를 부모 배열과 오버라이딩으로 제거하고 인스턴스화하면 의미 없는 부모를 추상 클래스로 제한합니다.
다형성의 실용적 가치는 여러 구현을 하나의 계약으로 처리하는 데 있습니다.
각 타입마다 비슷한 처리 함수를 복사하는 대신 부모 타입 매개변수 하나와 오버라이딩 메서드를 사용합니다.
공통 부모 자체로는 완전한 객체 의미가 없다면 abstract로 생성과 구현 누락을 막습니다.
이 절의 예제는 타입별 처리 중복, 부모 배열 통합, 추상 메서드 강제라는 세 단계로 같은 문제를 개선합니다. 코드 모양보다 새 활동을 추가할 때 기존 처리기가 얼마나 적게 바뀌는지에 주목합니다.
추상 클래스의 생성 제한
public final class InstantiateAbstractActivity {
public static void main(String[] args) {
StudyActivity activity = new StudyActivity("general", 10);
System.out.println(activity);
}
private abstract static class StudyActivity {
private final String topic;
private final int minutes;
StudyActivity(String topic, int minutes) {
this.topic = topic;
this.minutes = minutes;
}
abstract void perform();
}
}error: StudyActivity is abstract; cannot be instantiatedStudyActivity는 공통 상태와 계약을 정의하지만 “일반 학습”을 어떻게 수행할지 구현하지 않았습니다.
추상 클래스는 new로 직접 만들 수 없고 구체 자식 생성자의 부모 부분으로만 초기화됩니다.
부모 타입 변수와 매개변수로 사용하는 것은 가능합니다.
부모 매개변수와 공통 처리
다형성을 사용하지 않으면 다음과 같은 메서드가 늘어납니다.
static void processReading(ReadingActivity activity) { ... }
static void processCoding(CodingActivity activity) { ... }
static void processVideo(VideoActivity activity) { ... }공통 처리 순서가 같고 중간 행동만 다르면 부모 계약으로 합칩니다.
public final class PolymorphicProcessor {
public static void main(String[] args) {
process(new ReadingActivity("Java", 40));
process(new CodingActivity("Spring", 50));
}
private static void process(StudyActivity activity) {
System.out.println("start=" + activity.topic());
activity.perform();
System.out.println("saved=" + activity.minutes());
}
private abstract static class StudyActivity {
private final String topic;
private final int minutes;
StudyActivity(String topic, int minutes) {
this.topic = topic;
this.minutes = minutes;
}
String topic() { return topic; }
int minutes() { return minutes; }
abstract void perform();
}
private static final class ReadingActivity extends StudyActivity {
ReadingActivity(String topic, int minutes) { super(topic, minutes); }
@Override void perform() { System.out.println("read"); }
}
private static final class CodingActivity extends StudyActivity {
CodingActivity(String topic, int minutes) { super(topic, minutes); }
@Override void perform() { System.out.println("code"); }
}
}start=Java
read
saved=40
start=Spring
code
saved=50process는 새 활동 종류가 추가돼도 부모의 공개 API를 지키면 수정할 필요가 없습니다.
공통 시작·저장 흐름은 한 번만 구현되고 구체 수행만 각 자식이 제공합니다.
추상 타입 배열
public final class ActivityBatch {
public static void main(String[] args) {
StudyActivity[] activities = {
new ReadingActivity(40),
new CodingActivity(50),
new VideoActivity(30)
};
int total = 0;
for (StudyActivity activity : activities) {
System.out.println(activity.kind());
activity.perform();
total += activity.minutes();
}
System.out.println("total=" + total);
}
private abstract static class StudyActivity {
private final int minutes;
StudyActivity(int minutes) { this.minutes = minutes; }
int minutes() { return minutes; }
abstract String kind();
abstract void perform();
}
private static final class ReadingActivity extends StudyActivity {
ReadingActivity(int minutes) { super(minutes); }
@Override String kind() { return "reading"; }
@Override void perform() { System.out.println("read"); }
}
private static final class CodingActivity extends StudyActivity {
CodingActivity(int minutes) { super(minutes); }
@Override String kind() { return "coding"; }
@Override void perform() { System.out.println("code"); }
}
private static final class VideoActivity extends StudyActivity {
VideoActivity(int minutes) { super(minutes); }
@Override String kind() { return "video"; }
@Override void perform() { System.out.println("watch"); }
}
}reading
read
coding
code
video
watch
total=120배열 타입, 반복 변수, 합계 로직은 한 번만 존재합니다.
타입별 배열과 반복문을 따로 두는 중복이 사라졌습니다.
오버라이딩이 실제 객체의 행동을 선택하므로 switch나 instanceof 분기가 필요 없습니다.
추상 메서드
일반 부모 메서드에 빈 기본 구현을 두면 자식이 오버라이딩을 빼먹어도 컴파일됩니다.
void perform() {
// 아무 작업도 하지 않음
}새 활동이 조용히 아무것도 하지 않는 결함이 실행 중에 발견됩니다.
abstract void perform();은 본문이 없고 구체 자식이 반드시 구현해야 합니다.
구현하지 않은 자식도 abstract로 선언해 책임을 다음 하위 타입에 넘겨야 합니다.
추상 클래스는 추상 메서드뿐 아니라 생성자, 필드, 완성된 일반 메서드도 가질 수 있습니다.
공통 상태와 공통 구현을 재사용하면서 일부 행동만 자식에 맡길 때 적합합니다.
추상 메서드가 하나도 없어도 직접 생성을 막기 위해 abstract 클래스로 만들 수 있습니다.
추상 활동 저장
public final class PolymorphicStudyLog {
public static void main(String[] args) {
StudyLog log = new StudyLog(3);
log.add(new ReadingActivity("Java", 40));
log.add(new CodingActivity("Spring", 50));
log.performAll();
System.out.println(log.summary());
}
private static final class StudyLog {
private final StudyActivity[] entries;
private int size;
StudyLog(int capacity) { entries = new StudyActivity[capacity]; }
void add(StudyActivity activity) {
if (size == entries.length) throw new IllegalStateException("full");
entries[size++] = activity;
}
void performAll() {
for (int index = 0; index < size; index++) {
entries[index].perform();
}
}
String summary() {
int total = 0;
for (int index = 0; index < size; index++) {
total += entries[index].minutes();
}
return "count=" + size + ", total=" + total;
}
}
private abstract static class StudyActivity {
private final String topic;
private final int minutes;
StudyActivity(String topic, int minutes) { this.topic = topic; this.minutes = minutes; }
int minutes() { return minutes; }
String topic() { return topic; }
abstract void perform();
}
private static final class ReadingActivity extends StudyActivity {
ReadingActivity(String topic, int minutes) { super(topic, minutes); }
@Override void perform() { System.out.println("read=" + topic()); }
}
private static final class CodingActivity extends StudyActivity {
CodingActivity(String topic, int minutes) { super(topic, minutes); }
@Override void perform() { System.out.println("code=" + topic()); }
}
}read=Java
code=Spring
count=2, total=90StudyLog는 구체 자식 이름을 알지 못하고 StudyActivity 배열만 소유합니다.
새 활동 구현을 추가해도 저장·합계·일괄 수행 로직은 부모 타입에 정의된 기능을 그대로 사용합니다.
연습 문제
추상 ReportItem에 final 필드 title과 추상 메서드 render()를 두고 TextItem과 NumberItem이 구현하게 하세요.
부모 배열에서 두 항목을 렌더링합니다.
해설 보기
public final class AbstractReportItems {
public static void main(String[] args) {
ReportItem[] items = {
new TextItem("topic", "arrays"),
new NumberItem("minutes", 40)
};
for (ReportItem item : items) {
System.out.println(item.render());
}
}
private abstract static class ReportItem {
protected final String title;
ReportItem(String title) { this.title = title; }
abstract String render();
}
private static final class TextItem extends ReportItem {
private final String value;
TextItem(String title, String value) { super(title); this.value = value; }
@Override String render() { return title + "=" + value; }
}
private static final class NumberItem extends ReportItem {
private final int value;
NumberItem(String title, int value) { super(title); this.value = value; }
@Override String render() { return title + "=" + value; }
}
}topic=arrays
minutes=40공통 title은 부모가 초기화하고 값 형식별 렌더링은 구체 자식이 책임집니다.