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