다형적 참조
부모 타입 변수가 자신과 모든 하위 타입 객체를 참조하는 규칙과 정적 타입이 호출 가능한 멤버를 제한하는 이유를 실습합니다.
다형성은 한 객체가 여러 타입 관점으로 사용될 수 있는 능력입니다.
CodePost 객체는 CodePost이면서 부모인 Post이기도 합니다.
부모 타입 참조 하나로 여러 자식 구현을 담을 수 있지만, 호출 가능한 멤버는 참조 변수의 컴파일 시점 타입에서 시작해 결정합니다.
부모 참조의 접근 범위
public final class PolymorphicReferenceLimit {
public static void main(String[] args) {
Post post = new CodePost();
post.commit();
}
private static class Post {
void publish() {
System.out.println("publish");
}
}
private static final class CodePost extends Post {
void commit() {
System.out.println("commit");
}
}
}error: cannot find symbol
symbol: method commit()
location: variable post of type Post실제 객체 안에는 CodePost 부분과 commit 메서드가 있습니다.
그러나 컴파일러는 post의 선언 타입인 Post에서 호출 가능한 API를 확인합니다.
부모 방향으로는 메서드를 찾을 수 있지만 자식 방향으로 임의로 내려갈 수 없습니다.
어느 자식인지 보장되지 않기 때문입니다.
부모 타입과 하위 타입
public final class PolymorphicReferences {
public static void main(String[] args) {
Post base = new Post("공지");
Post textPost = new TextPost("가입 인사");
Post codePost = new CodePost("로그인 구현");
base.publish();
textPost.publish();
codePost.publish();
}
private static class Post {
private final String title;
Post(String title) {
this.title = title;
}
void publish() {
System.out.println("board=" + title);
}
}
private static final class TextPost extends Post {
TextPost(String title) {
super(title);
}
}
private static final class CodePost extends Post {
CodePost(String title) {
super(title);
}
}
}board=공지
board=가입 인사
board=로그인 구현세 변수의 정적 타입은 모두 Post지만 실제 참조 대상은 부모, TextPost, CodePost로 다릅니다.
부모 타입은 자신과 자식, 손자 등 모든 하위 타입을 담을 수 있습니다.
반대인 자식 타입 변수에 부모 객체를 자동 대입할 수는 없습니다.
자식에서 부모로 보는 업캐스팅은 안전해 자동 적용됩니다.
CodePost child = new CodePost("로그인 구현");
Post parentView = child;parentView가 생겨도 객체가 복사되거나 변환되지 않습니다.
같은 참조값을 부모 타입 변수에 담아 볼 수 있는 공개 API의 범위만 달라집니다.
변수 타입과 객체 타입
Post post = new CodePost("로그인 구현");
^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
변수의 정적 타입 실제 객체의 구체 타입컴파일 단계는 정적 타입으로 메서드 존재와 접근 가능성을 검사합니다.
실행 단계는 실제 객체를 찾고, 호출한 메서드가 오버라이딩됐다면 가장 구체적인 구현을 선택합니다.
아직 오버라이딩이 없다면 부모 구현이 그대로 실행됩니다.
필드 접근과 static 메서드는 인스턴스 오버라이딩과 같은 동적 선택을 하지 않습니다.
다형성의 핵심은 인스턴스 메서드 오버라이딩입니다.
상태는 private으로 숨기고 행동 계약을 통해 다형성을 사용하면 필드 숨김 혼란을 줄일 수 있습니다.
부모 타입 배열
public final class PolymorphicPostArray {
public static void main(String[] args) {
Post[] posts = {
new TextPost("가입 인사", 40),
new CodePost("로그인 구현", 50),
new Post("공지", 20)
};
int total = 0;
for (Post post : posts) {
System.out.println(post.describe());
total += post.viewCount();
}
System.out.println("total=" + total);
}
private static class Post {
private final String title;
private final int viewCount;
Post(String title, int viewCount) {
this.title = title;
this.viewCount = viewCount;
}
String describe() {
return "board=" + title;
}
int viewCount() {
return viewCount;
}
}
private static final class TextPost extends Post {
TextPost(String title, int viewCount) {
super(title, viewCount);
}
@Override
String describe() {
return "text=" + super.describe();
}
}
private static final class CodePost extends Post {
CodePost(String title, int viewCount) {
super(title, viewCount);
}
@Override
String describe() {
return "code=" + super.describe();
}
}
}text=board=가입 인사
code=board=로그인 구현
board=공지
total=110배열 원소 타입 하나로 세 구체 타입을 담았습니다.
반복문은 자식 타입별 분기 없이 공통 describe와 viewCount만 호출합니다.
describe는 실제 객체에 따라 오버라이딩 구현이 선택되고 viewCount는 부모 구현을 공유합니다.
다형적 참조의 한계
다형적 참조는 다음을 제공합니다.
- 부모 타입 컬렉션에 여러 하위 타입 저장
- 부모 매개변수 하나로 여러 구현 전달
- 공통 계약만 아는 클라이언트 작성
- 오버라이딩과 결합해 구체 동작 선택
반면 다음을 자동으로 제공하지 않습니다.
- 자식 전용 메서드를 부모 참조에서 호출
- 관련 없는 타입 사이 변환
- 잘못된 다운캐스팅의 안전성
- 공통 부모에 없는 행동의 통일
자식 전용 행동이 반복적으로 필요하다면 캐스팅부터 추가하기보다 그 행동이 부모 계약에 속해야 하는지, 별도 인터페이스 역할로 분리해야 하는지 검토합니다.
다형적 매개변수
public final class PostProcessor {
public static void main(String[] args) {
process(new TextPost());
process(new CodePost());
}
private static void process(Post post) {
System.out.println("before");
post.publish();
System.out.println("after");
}
private static class Post {
void publish() {
System.out.println("publish general post");
}
}
private static final class TextPost extends Post {
@Override
void publish() {
System.out.println("publish text post");
}
}
private static final class CodePost extends Post {
@Override
void publish() {
System.out.println("publish code post");
}
}
}before
publish text post
after
before
publish code post
afterprocess는 자식 클래스 이름을 알지 못하고 Post의 공개 API에만 의존합니다.
새 게시글이 부모를 올바르게 확장하면 process 코드를 바꾸지 않고 전달할 수 있습니다.
연습 문제
Post 부모와 TextPost, CodePost 자식을 만들고 부모 타입 배열의 총 viewCount를 계산하세요.
각 자식이 kind()를 오버라이딩해 text, code를 반환하게 합니다.
해설 보기
public final class PolymorphicReport {
public static void main(String[] args) {
Post[] posts = {
new TextPost(40),
new CodePost(50)
};
int total = 0;
for (Post post : posts) {
System.out.println(post.kind());
total += post.viewCount;
}
System.out.println("total=" + total);
}
private static class Post {
private final int viewCount;
Post(int viewCount) {
this.viewCount = viewCount;
}
String kind() {
return "board";
}
}
private static final class TextPost extends Post {
TextPost(int viewCount) { super(viewCount); }
@Override String kind() { return "text"; }
}
private static final class CodePost extends Post {
CodePost(int viewCount) { super(viewCount); }
@Override String kind() { return "code"; }
}
}text
code
total=90반복문은 부모 타입만 사용하지만 실제 객체의 kind 구현이 선택됩니다.
공통 viewCount는 부모에 한 번 정의됩니다.