업캐스팅과 다운캐스팅
자동 업캐스팅과 명시적 다운캐스팅의 방향을 비교하고 실제 객체에 없는 자식 부분을 요구할 때의 ClassCastException을 재현합니다.
캐스팅 방향은 상속 계층에서 이동하는 타입 관점의 방향입니다.
자식에서 부모로 넓히는 업캐스팅은 실제 객체 안에 부모 부분이 항상 있어 안전합니다.
부모에서 자식으로 좁히는 다운캐스팅은 실제 객체가 그 자식인지 확인되지 않으므로 실행 실패 가능성이 있습니다.
자식 변수에 보관하지 않고 ((CodePost) post).commit()처럼 한 표현식에서만 쓰는 방식을 일시적 다운 캐스팅이라고 합니다.
문법이 짧아져도 실제 타입이 다를 때의 위험은 같으므로 호환성 검사가 먼저입니다.
잘못된 다운캐스팅
public final class InvalidDowncast {
public static void main(String[] args) {
Post post = new Post();
CodePost codePost = (CodePost) post;
codePost.commit();
}
private static class Post {}
private static final class CodePost extends Post {
void commit() {
System.out.println("commit");
}
}
}Exception in thread "main" java.lang.ClassCastExceptionpost의 실제 객체는 Post 자체이므로 그 안에 CodePost 부분이 없습니다.
명시적 캐스트는 개발자가 책임지겠다는 표시일 뿐 존재하지 않는 자식 부분을 만들지 않습니다.
런타임 타입 검사가 실패해 예외가 납니다.
컴파일러가 캐스트 문법을 허용하는 이유는 다른 실행에서는 post가 CodePost 객체를 담을 수도 있기 때문입니다.
실행 값에 따라 성공 여부가 달라져 런타임 검사가 필요합니다.
업캐스팅의 안전성
public final class SafeUpcasting {
public static void main(String[] args) {
FeaturedCodePost featured = new FeaturedCodePost();
CodePost codePost = featured;
Post post = featured;
codePost.commit();
post.publish();
System.out.println("same=" + (featured == codePost && codePost == post));
}
private static class Post {
void publish() {
System.out.println("publish");
}
}
private static class CodePost extends Post {
void commit() {
System.out.println("commit");
}
}
private static final class FeaturedCodePost extends CodePost {
void pinToTop() {
System.out.println("pinned");
}
}
}commit
publish
same=trueFeaturedCodePost 인스턴스에는 자신과 부모 CodePost, Post에 해당하는 상태와 행동이 함께 있습니다.
어느 부모 관점으로 보더라도 필요한 부분이 실제 객체에 존재합니다.
그래서 (Post) featured처럼 명시할 필요 없이 자동 업캐스팅합니다.
업캐스팅하면 자식 전용 pinToTop이 삭제되는 것이 아니라 정적 타입 계약에서 보이지 않을 뿐입니다.
올바른 실제 타입을 알고 다시 다운캐스팅하면 접근할 수 있습니다.
다운캐스팅 성공 조건
public final class CastOutcomeTable {
public static void main(String[] args) {
Post first = new CodePost();
Post second = new TextPost();
printCast(first);
printCast(second);
}
private static void printCast(Post post) {
try {
CodePost codePost = (CodePost) post;
codePost.commit();
} catch (ClassCastException exception) {
System.out.println("not-code-post=" + post.getClass().getSimpleName());
}
}
private static class Post {}
private static final class CodePost extends Post {
void commit() {
System.out.println("commit");
}
}
private static final class TextPost extends Post {}
}commit
not-code-post=TextPost변수 선언 Post post만 보면 결과를 알 수 없습니다.
그 참조가 어떤 new 표현식에서 만들어진 객체를 가리키는지 추적해야 합니다.
첫 참조는 CodePost여서 성공하고 둘째는 형제 타입 TextPost여서 실패합니다.
예외를 일반 제어 흐름으로 사용하는 것보다 instanceof로 먼저 검사하는 편이 의도를 드러냅니다.
다음 문서에서 패턴 매칭까지 연결합니다.
상속 관계와 캐스팅
관련 없는 String과 Post 사이를 캐스팅해 객체 종류를 바꿀 수 없습니다.
컴파일러가 타입 관계상 절대 가능하지 않은 캐스트를 차단합니다.
Object처럼 공통 상위 타입을 거쳐 문법을 우회하더라도 실제 객체 호환성이 없으면 런타임에 실패합니다.
기본형 숫자 캐스팅과 참조형 캐스팅도 의미가 다릅니다.
- 숫자 캐스팅은 값 표현을 바꾸며 정보가 잘릴 수 있습니다.
- 참조 캐스팅은 같은 객체 참조를 다른 타입 관점으로 검사·해석합니다.
- 참조 캐스팅은 객체 필드를 복사하거나 새 자식 객체를 만들지 않습니다.
다운캐스팅의 경계
public final class BoundaryDowncast {
public static void main(String[] args) {
Post[] posts = {
new CodePost("feature/login"),
new TextPost()
};
for (Post post : posts) {
exportCodeDetail(post);
}
}
private static void exportCodeDetail(Post post) {
if (!(post instanceof CodePost)) {
System.out.println("skip-non-code-post");
return;
}
CodePost codePost = (CodePost) post;
System.out.println("branch=" + codePost.branch);
}
private static class Post {}
private static final class CodePost extends Post {
private final String branch;
CodePost(String branch) { this.branch = branch; }
}
private static final class TextPost extends Post {}
}branch=feature/login
skip-non-code-post구체 타입 세부 정보를 내보내는 경계 한 곳에 검사와 캐스팅을 모았습니다.
핵심 합계나 실행 로직은 부모 계약과 오버라이딩으로 처리하고, 자식 전용 정보가 실제로 필요한 출력 경계만 좁힙니다.
연습 문제
Post → CodePost → FeaturedCodePost 계층을 만들고 가장 구체적인 객체를 부모 변수에 담으세요.
CodePost와 FeaturedCodePost로 각각 안전하게 다운캐스팅해 메서드를 호출합니다.
해설 보기
public final class MultiLevelCast {
public static void main(String[] args) {
Post post = new FeaturedCodePost();
CodePost codePost = (CodePost) post;
codePost.commit();
FeaturedCodePost featured = (FeaturedCodePost) codePost;
featured.pinToTop();
}
private static class Post {}
private static class CodePost extends Post {
void commit() { System.out.println("commit"); }
}
private static final class FeaturedCodePost extends CodePost {
void pinToTop() { System.out.println("pinned"); }
}
}commit
pinned실제 객체가 가장 구체적인 FeaturedCodePost이므로 두 부모 관점에서 다시 좁혀도 호환됩니다.