업캐스팅과 다운캐스팅
자동 업캐스팅과 명시적 다운캐스팅의 방향을 비교하고 실제 객체에 없는 자식 부분을 요구할 때의 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이 삭제되는 것이 아니라 정적 타입 계약에서 보이지 않을 뿐입니다.
올바른 실제 타입을 알고 다시 다운캐스팅하면 접근할 수 있습니다.
SafeUpcasting의 featured, codePost, post는 서로 다른 정적 타입의 지역 참조이지만 모두 하나의 FeaturedCodePost 객체를 가리키며 같음 비교는true다. 타입 관점은 바뀌지만 객체가 복제되거나 기능이 사라지지 않는다.
SafeUpcasting의 세 지역 참조입니다. 본문의 세 메서드가 각 정적 타입에서 보이는 범위를 비교하며, 선은 메서드 호출이 아니라 같은 객체를 가리키는 참조입니다.
featured· 정적 타입FeaturedCodePostpublish(),commit(),pinToTop()이 보입니다.codePost· 정적 타입CodePostpublish(),commit()이 보입니다.featured와 같은 객체를 가리킵니다.post· 정적 타입Postpublish()이 보입니다. 앞의 두 참조와 같은 객체를 가리킵니다.- 실제 객체는 하나
- 세 참조 모두
FeaturedCodePost객체에 도달합니다. 업캐스팅으로 객체가 복제되거나pinToTop()이 삭제되지는 않습니다.
원문은 codePost.commit()으로 commit, post.publish()으로 publish, 세 참조의 같음 비교로 same=true를 출력합니다. pinToTop()은 선언되어 있지만 이 main에서 호출하지 않습니다.
다운캐스팅 성공 조건
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로 먼저 검사하는 편이 의도를 드러냅니다.
다음 문서에서 패턴 매칭까지 연결합니다.
InvalidDowncast의 Post 객체는 CodePost 캐스트에서 처리되지 않은 예외로 끝난다. 별도 CastOutcomeTable의 CodePost는 commit을 실행하고 TextPost는 캐스트 실패를 잡아 not-code-post를 출력한다.
세 경우 모두 캐스트 직전 참조의 정적 타입은 Post이고 대상은 CodePost입니다. 첫 행과 나머지 두 행은 서로 다른 프로그램입니다.
| 원문 호출 | 실제 객체 | 캐스트 이후 |
|---|---|---|
InvalidDowncast | new Post() | ClassCastException이 밖으로 전파됩니다.
|
CastOutcomeTable첫 번째 | new CodePost() | 캐스트 성공 후 commit()을 호출합니다.출력: |
CastOutcomeTable두 번째 | new TextPost() | ClassCastException을 같은 메서드의 catch가 처리합니다.
|
InvalidDowncast·new Post()CodePost캐스트에서ClassCastException이 밖으로 전파됩니다.codePost에 참조를 대입하지 못하고commit()호출에 도달하지 않습니다.CastOutcomeTable의 첫 호출 ·new CodePost()캐스트 성공 후
commit()을 호출하여commit을 출력합니다.CastOutcomeTable의 둘째 호출 ·new TextPost()캐스트 실패를 같은
printCast의catch가 처리합니다.commit()은 호출하지 않고not-code-post=TextPost를 출력합니다.
성공한 참조 캐스트도 새 자식 객체를 만들지 않습니다. 실제 객체와의 호환성을 확인할 뿐이며, 뒤의 BoundaryDowncast는 예외 대신 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이므로 두 부모 관점에서 다시 좁혀도 호환됩니다.