본문 바로가기

instanceof와 오버라이딩

instanceof와 패턴 매칭으로 다운캐스팅 경계를 안전하게 만들고 실제 객체의 오버라이딩 메서드가 우선되는 실행 규칙을 추적합니다.

다운캐스팅 전에 실제 객체가 원하는 타입과 호환되는지 확인하면 ClassCastException을 피할 수 있습니다.

instanceof는 이 검사를 표현하고, 패턴 매칭은 성공한 범위에서 자식 변수를 함께 만듭니다.

다형성의 더 중요한 축은 부모 참조에서도 실제 객체의 오버라이딩 메서드가 실행된다는 점입니다.


@Override 검증

lab/BadOverrideSignature.java
public final class BadOverrideSignature {
    private static class Post {
        void publish() {
            System.out.println("board");
        }
    }

    private static final class CodePost extends Post {
        @Override
        void publish(int viewCount) {
            System.out.println("code=" + viewCount);
        }
    }
}
컴파일 실패 관찰
error: method does not override or implement a method from a supertype

매개변수 없는 부모 publish와 int 매개변수가 있는 자식 publish는 서로 다른 메서드입니다.

@Override가 없으면 새 오버로드로 컴파일되어 부모 참조 호출은 계속 부모 구현을 실행합니다.

애노테이션을 붙이면 개발자의 재정의 의도와 실제 서명이 다를 때 즉시 실패합니다.

오버라이딩 조건에는 같은 이름과 매개변수 목록, 호환되는 반환 타입, 부모보다 좁지 않은 접근 범위가 포함됩니다.

private 메서드는 자식에서 보이지 않으므로 오버라이딩 대상이 아닙니다.

static 메서드는 인스턴스 동적 디스패치가 아니라 클래스 기준으로 숨겨집니다.


instanceof 타입 검사

src/SafeInstanceof.java
public final class SafeInstanceof {
    public static void main(String[] args) {
        inspect(new CodePost("feature/login"));
        inspect(new TextPost(120));
    }

    private static void inspect(Post post) {
        if (post instanceof CodePost) {
            CodePost codePost = (CodePost) post;
            System.out.println("branch=" + codePost.branch);
        } else if (post instanceof TextPost) {
            TextPost textPost = (TextPost) post;
            System.out.println("bodyLength=" + textPost.bodyLength);
        }
    }

    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 {
        private final int bodyLength;
        TextPost(int bodyLength) { this.bodyLength = bodyLength; }
    }
}
branch=feature/login
bodyLength=120

post instanceof CodePost는 참조가 null이 아니고 실제 객체가 CodePost 또는 그 하위 타입이면 true입니다.

성공한 분기 안에서 다운캐스팅이 안전합니다.

null은 어떤 참조 타입 instanceof에도 false이므로 별도 캐스팅 예외가 나지 않습니다.


패턴 매칭

src/PatternMatchingInstanceof.java
public final class PatternMatchingInstanceof {
    public static void main(String[] args) {
        printDetail(new CodePost("feature/login"));
        printDetail(new Post());
    }

    private static void printDetail(Post post) {
        if (post instanceof CodePost codePost) {
            System.out.println("branch=" + codePost.branch());
            return;
        }
        System.out.println("general");
    }

    private static class Post {}

    private static final class CodePost extends Post {
        private final String branch;
        CodePost(String branch) { this.branch = branch; }
        String branch() { return branch; }
    }
}
branch=feature/login
general

패턴 변수 codePost는 instanceof가 true인 경로에서만 존재합니다.

실패한 경로에서는 CodePost라는 보장이 없으므로 사용할 수 없습니다.

조기 반환과 결합하면 이후 코드에서 반대 조건이 보장되는 흐름도 컴파일러가 분석합니다.

위 예제는 성공한 블록 안에서 반환하므로 블록 아래에 codePost가 없습니다. 반대로 실패한 경로를 먼저 반환시키면 이후 문장에 성공 조건만 남길 수 있습니다. 같은 PatternMatchingInstanceof에서 printDetail만 다음처럼 바꾸면 두 main 입력의 출력은 같습니다.

실패 경로를 먼저 끝내는 printDetail
private static void printDetail(Post post) {
    if (!(post instanceof CodePost codePost)) {
        System.out.println("general");
        return;
    }
    System.out.println("branch=" + codePost.branch());
}

null과 일반 Post는 실패 분기에서 끝나며, 마지막 출력문에 도달한 참조는 CodePost와 호환됩니다. 실패 분기의 return을 빼면 일반 입력도 마지막 문장에 도달할 수 있으므로 그 위치에서 패턴 변수를 사용할 수 없습니다. 단순 중괄호 개수보다 “어떤 경로가 여기까지 도달하는가”를 봅니다.

패턴 매칭은 캐스팅 문법 중복을 줄이지만 구체 타입 분기 자체를 없애지는 않습니다.

게시글 종류가 늘 때마다 분기를 수정한다면 공통 오버라이딩 메서드로 옮길 수 있는 행동인지 검토합니다.

패턴 검사 성공 경로의 변수와 조기 반환 범위

instanceof가 성공하면 패턴 변수로 branch 출력 후 return하고 실패/null은 general만 출력합니다. 도달 경로와 패턴 변수 사용 가능 범위가 실제 제어 전환으로 드러납니다.

패턴 검사 성공 경로의 변수와 조기 반환 범위

PatternMatchingInstanceof의 CodePost 입력은 성공 블록에서 codePost를 사용하고 반환한다. Post 입력은 블록을 건너뛰고 general을 출력하며 이 위치에 codePost는 범위 밖이다.

이 예제의 codePost는 성공 블록 안에서 사용합니다

PatternMatchingInstanceof.printDetail(Post post)의 검사입니다. 조건은 post instanceof CodePost codePost이고, 성공 블록 안에 return이 있습니다.

두 main 입력의 패턴 변수 범위와 출력·반환
실제 입력과 조건codePost 범위출력과 종료
CodePost

branch = "feature/login"

true

성공 블록에서 같은 객체를 가리키는 codePost를 사용할 수 있습니다.branch=feature/login 출력 후 반환합니다.

아래 general 출력에는 도달하지 않습니다.

new Post()

false

성공 블록을 건너뜁니다. 아래 문장에서는 codePost를 사용할 수 없습니다.general을 출력하고 메서드가 끝납니다.
이 예제의 codePost는 성공 블록 안에서 사용합니다 CodePost true→성공블록 codePost변수branch호출→return,일반Post false→general/변수범위밖;성공은아래general과합류없음/null false이나원main2입력만. printDetail( Post post) 원문의 두 독립 호출 CodePost와 호환되는가? true false 성공 블록 branch=feature/login codePost 사용 가능 return · 이후 출력 없음 general 출력 codePost 범위 밖 메서드 종료 성공과 실패는 아래 문장에 합류하지 않음 · null도 false
이 예제의 codePost는 성공 블록 안에서 사용합니다 CodePost true→성공블록 codePost변수branch호출→return,일반Post false→general/변수범위밖;성공은아래general과합류없음/null false이나원main2입력만. printDetail( Post post) CodePost 또는 Post 입력 CodePost와 호환되는가? true false 성공 블록 codePost 변수 사용 branch=feature/login 같은 객체를 참조 return 아래 출력에 합류 안 함 general 출력 codePost 범위 밖 null은 실패 경로로 갑니다
  1. new CodePost("feature/login") · 성공

    조건이 true인 블록에서 같은 객체의 참조를 가진 codePost로 branch()를 호출합니다. branch=feature/login 출력 후 반환하므로 아래 general은 실행하지 않습니다.

  2. new Post() · 실패

    조건이 false여서 성공 블록을 건너뜁니다. 아래 문장에서 codePost는 범위 밖이고, general을 출력한 뒤 메서드가 끝납니다.

null도 이 패턴 검사에서는 false입니다. 다만 위 main은 null을 전달하지 않습니다. 이 예제에서는 성공 경로가 먼저 반환하므로 두 경로가 아래 출력 문장에 합류하지 않습니다.

이 예의 조건

  • main은 CodePost와 일반 Post를 호출하며 null false는 미실행 조건입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2CodePost true→성공 블록의 codePost 변수로 branch 호출→return, 일반 Post false→general/변수 범위 밖이다. 성공 경로는 아래 general과 합류하지 않는다. null도 false이지만 원 main에는 2개 입력만 있다.
원 도표의 설명과 주석
구분내용
설명CodePost true→성공 블록의 codePost 변수로 branch 호출→return, 일반 Post false→general/변수 범위 밖이다. 성공 경로는 아래 general과 합류하지 않는다. null도 false이지만 원 main에는 2개 입력만 있다.
전체 읽기 정보패턴 검사 성공 경로의 변수와 조기 반환 범위 패턴 검사 성공 경로의 변수와 조기 반환 범위 PatternMatchingInstanceof의 CodePost 입력은 성공 블록에서 codePost를 사용하고 반환한다. Post 입력은 블록을 건너뛰고 general을 출력하며 이 위치에서 codePost는 범위 밖이다. 이 예제의 codePost는 성공 블록 안에서 사용합니다 PatternMatchingInstanceof.printDetail(Post post) 의 검사입니다. 조건은 post instanceof CodePost codePost 이고, 성공 블록 안에 return 이 있습니다. 두 main 입력의 패턴 변수 범위와 출력·반환 실제 입력과 조건 codePost 범위 출력과 종료 CodePost branch = "feature/login" true 성공 블록에서 같은 객체를 가리키는 codePost 를 사용할 수 있습니다. branch=feature/login 출력 후 반환합니다. 아래 general 출력에는 도달하지 않습니다. new Post() false 성공 블록을 건너뜁니다. 아래 문장에서는 codePost 를 사용할 수 없습니다. general 을 출력하고 메서드가 끝납니다. new CodePost("feature/login") · 성공 조건이 true 인 블록에서 같은 객체의 참조를 가진 codePost 로 branch() 를 호출합니다. branch=feature/login 출력 후 반환하므로 아래 general 은 실행하지 않습니다. new Post() · 실패 조건이 false 여서 성공 블록을 건너뜁니다. 아래 문장에서 codePost 는 범위 밖이고, general 을 출력한 뒤 메서드가 끝납니다. null 도 이 패턴 검사에서는 false 입니다. 다만 위 main 은 null 을 전달하지 않습니다. 이 예제에서는 성공 경로가 먼저 반환하므로 두 경로가 아래 출력 문장에 합류하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
패턴 검사

post가 CodePost와 호환되는지 검사합니다.

  • CodePost: true
  • 일반 Post: false
  • 성공 조건이 패턴 변수의 사용 범위를 만듭니다.
성공 블록

codePost로 branch를 읽고 return합니다.

  • 출력: branch=feature/login
  • 다음 general: 미실행
  • 성공 경로는 아래 일반 출력에 합류하지 않습니다.
실패 경로

일반 Post는 general을 출력하고 끝납니다.

  • codePost: 범위 밖
  • null 조건: false
  • 성공 블록 밖에서 패턴 변수를 쓸 수 없습니다.

패턴 검사 성공 경로의 변수와 조기 반환 범위

instanceof가 성공하면 패턴 변수로 branch 출력 후 return하고 실패/null은 general만 출력합니다. 도달 경로와 패턴 변수 사용 가능 범위가 실제 제어 전환으로 드러납니다.

패턴 검사 성공 경로의 변수와 조기 반환 범위

PatternMatchingInstanceof의 CodePost 입력은 성공 블록에서 codePost를 사용하고 반환한다. Post 입력은 블록을 건너뛰고 general을 출력하며 이 위치에 codePost는 범위 밖이다.

이 예제의 codePost는 성공 블록 안에서 사용합니다

PatternMatchingInstanceof.printDetail(Post post)의 검사입니다. 조건은 post instanceof CodePost codePost이고, 성공 블록 안에 return이 있습니다.

두 main 입력의 패턴 변수 범위와 출력·반환
실제 입력과 조건codePost 범위출력과 종료
CodePost

branch = "feature/login"

true

성공 블록에서 같은 객체를 가리키는 codePost를 사용할 수 있습니다.branch=feature/login 출력 후 반환합니다.

아래 general 출력에는 도달하지 않습니다.

new Post()

false

성공 블록을 건너뜁니다. 아래 문장에서는 codePost를 사용할 수 없습니다.general을 출력하고 메서드가 끝납니다.
이 예제의 codePost는 성공 블록 안에서 사용합니다 CodePost true→성공블록 codePost변수branch호출→return,일반Post false→general/변수범위밖;성공은아래general과합류없음/null false이나원main2입력만. printDetail( Post post) 원문의 두 독립 호출 CodePost와 호환되는가? true false 성공 블록 branch=feature/login codePost 사용 가능 return · 이후 출력 없음 general 출력 codePost 범위 밖 메서드 종료 성공과 실패는 아래 문장에 합류하지 않음 · null도 false
이 예제의 codePost는 성공 블록 안에서 사용합니다 CodePost true→성공블록 codePost변수branch호출→return,일반Post false→general/변수범위밖;성공은아래general과합류없음/null false이나원main2입력만. printDetail( Post post) CodePost 또는 Post 입력 CodePost와 호환되는가? true false 성공 블록 codePost 변수 사용 branch=feature/login 같은 객체를 참조 return 아래 출력에 합류 안 함 general 출력 codePost 범위 밖 null은 실패 경로로 갑니다
  1. new CodePost("feature/login") · 성공

    조건이 true인 블록에서 같은 객체의 참조를 가진 codePost로 branch()를 호출합니다. branch=feature/login 출력 후 반환하므로 아래 general은 실행하지 않습니다.

  2. new Post() · 실패

    조건이 false여서 성공 블록을 건너뜁니다. 아래 문장에서 codePost는 범위 밖이고, general을 출력한 뒤 메서드가 끝납니다.

null도 이 패턴 검사에서는 false입니다. 다만 위 main은 null을 전달하지 않습니다. 이 예제에서는 성공 경로가 먼저 반환하므로 두 경로가 아래 출력 문장에 합류하지 않습니다.

이 예의 조건

  • main은 CodePost와 일반 Post를 호출하며 null false는 미실행 조건입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2CodePost true→성공 블록의 codePost 변수로 branch 호출→return, 일반 Post false→general/변수 범위 밖이다. 성공 경로는 아래 general과 합류하지 않는다. null도 false이지만 원 main에는 2개 입력만 있다.
원 도표의 설명과 주석
구분내용
설명CodePost true→성공 블록의 codePost 변수로 branch 호출→return, 일반 Post false→general/변수 범위 밖이다. 성공 경로는 아래 general과 합류하지 않는다. null도 false이지만 원 main에는 2개 입력만 있다.
전체 읽기 정보패턴 검사 성공 경로의 변수와 조기 반환 범위 패턴 검사 성공 경로의 변수와 조기 반환 범위 PatternMatchingInstanceof의 CodePost 입력은 성공 블록에서 codePost를 사용하고 반환한다. Post 입력은 블록을 건너뛰고 general을 출력하며 이 위치에서 codePost는 범위 밖이다. 이 예제의 codePost는 성공 블록 안에서 사용합니다 PatternMatchingInstanceof.printDetail(Post post) 의 검사입니다. 조건은 post instanceof CodePost codePost 이고, 성공 블록 안에 return 이 있습니다. 두 main 입력의 패턴 변수 범위와 출력·반환 실제 입력과 조건 codePost 범위 출력과 종료 CodePost branch = "feature/login" true 성공 블록에서 같은 객체를 가리키는 codePost 를 사용할 수 있습니다. branch=feature/login 출력 후 반환합니다. 아래 general 출력에는 도달하지 않습니다. new Post() false 성공 블록을 건너뜁니다. 아래 문장에서는 codePost 를 사용할 수 없습니다. general 을 출력하고 메서드가 끝납니다. new CodePost("feature/login") · 성공 조건이 true 인 블록에서 같은 객체의 참조를 가진 codePost 로 branch() 를 호출합니다. branch=feature/login 출력 후 반환하므로 아래 general 은 실행하지 않습니다. new Post() · 실패 조건이 false 여서 성공 블록을 건너뜁니다. 아래 문장에서 codePost 는 범위 밖이고, general 을 출력한 뒤 메서드가 끝납니다. null 도 이 패턴 검사에서는 false 입니다. 다만 위 main 은 null 을 전달하지 않습니다. 이 예제에서는 성공 경로가 먼저 반환하므로 두 경로가 아래 출력 문장에 합류하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
패턴 검사

post가 CodePost와 호환되는지 검사합니다.

  • CodePost: true
  • 일반 Post: false
  • 성공 조건이 패턴 변수의 사용 범위를 만듭니다.
성공 블록

codePost로 branch를 읽고 return합니다.

  • 출력: branch=feature/login
  • 다음 general: 미실행
  • 성공 경로는 아래 일반 출력에 합류하지 않습니다.
실패 경로

일반 Post는 general을 출력하고 끝납니다.

  • codePost: 범위 밖
  • null 조건: false
  • 성공 블록 밖에서 패턴 변수를 쓸 수 없습니다.

동적 메서드 탐색

src/OverrideDispatch.java
public final class OverrideDispatch {
    public static void main(String[] args) {
        Post first = new TextPost();
        Post second = new CodePost();

        run(first);
        run(second);
    }

    private static void run(Post post) {
        System.out.println(post.publish());
    }

    private static class Post {
        String publish() {
            return "general";
        }
    }

    private static final class TextPost extends Post {
        @Override
        String publish() {
            return "text";
        }
    }

    private static final class CodePost extends Post {
        @Override
        String publish() {
            return "code";
        }
    }
}
text
code

run의 매개변수 타입은 Post이므로 publish 호출이 컴파일됩니다.

실행할 때 첫 번째 객체는 TextPost 구현을, 두 번째 객체는 CodePost 구현을 선택합니다.

메서드 오버라이딩은 다형적 참조보다 우선해 실제 객체의 가장 구체적인 구현을 실행합니다.

이 규칙 덕분에 클라이언트가 if (post instanceof ...)로 종류를 나누지 않아도 됩니다.

새 자식 타입은 publish 규칙을 구현하고 기존 run에 그대로 전달됩니다.


필드와 static 메서드

src/FieldAndMethodDispatch.java
public final class FieldAndMethodDispatch {
    public static void main(String[] args) {
        Parent value = new Child();

        System.out.println("field=" + value.name);
        System.out.println("method=" + value.name());
        System.out.println("static=" + value.staticName());
    }

    private static class Parent {
        String name = "parent";
        String name() { return "parent"; }
        static String staticName() { return "parent"; }
    }

    private static final class Child extends Parent {
        String name = "child";
        @Override String name() { return "child"; }
        static String staticName() { return "child"; }
    }
}
field=parent
method=child
static=parent

필드와 static 메서드는 참조 변수의 정적 타입 Parent를 기준으로 선택되고, 인스턴스 메서드만 실제 Child 객체의 오버라이딩을 선택합니다.

따라서 다형적 행동은 필드가 아니라 인스턴스 메서드로 표현해야 합니다.

정적 메서드는 인스턴스를 통해 호출할 수도 있지만 Parent.staticName()처럼 클래스 이름으로 쓰면 선택 기준이 더 잘 보입니다. 같은 객체를 Child childView = (Child) value;로 참조하면 childView.name은 child 필드를, childView.staticName()은 Child의 정적 메서드를 선택합니다. 객체가 바뀐 결과가 아니라 각 표현식의 정적 타입이 달라진 결과입니다.

세 결과를 예측하기: 부모 관점으로 다시 본다면?

새 객체를 만들지 않고 (Parent) childView를 사용합니다. ((Parent) childView).name, ((Parent) childView).name(), Parent.staticName()은 차례로 어떤 문자열을 주나요?

풀이: parent, child, parent입니다. 필드 접근 표현식의 타입은 Parent이고, 인스턴스 메서드 호출의 실제 수신 객체는 계속 Child이며, 마지막은 Parent에 선언한 정적 메서드를 직접 호출합니다.

Parent 참조의 필드·정적 메서드와 Child 인스턴스 메서드

FieldAndMethodDispatch에서 Parent value는 Child 객체를 가리킨다. name 필드와 staticName은 Parent 기준이고 name 인스턴스 메서드는 Child의 재정의를 선택한다.

필드·static과 인스턴스 메서드는 선택 기준이 다릅니다

FieldAndMethodDispatch의 선언은 Parent value = new Child();입니다. value의 정적 타입은 Parent, 참조 대상의 실제 타입은 Child입니다.

같은 value 표현식에서 멤버 종류에 따라 달라지는 선택
연산선택 기준선택된 멤버와 출력
value.name

필드

참조 표현식의 정적 타입 ParentParent에 선언된 name

field=parent

value.name()

인스턴스 메서드

Parent의 메서드 계약으로 컴파일한 뒤 실제 Child의 재정의를 선택Child.name()

method=child

value.staticName()

정적 메서드

참조 표현식의 정적 타입 Parent

인스턴스 동적 디스패치가 아님

Parent.staticName()

static=parent

필드·static과 인스턴스 메서드는 선택 기준이 다릅니다 동일value 정적Parent/실제Child;필드Parent.name fieldparent,인스턴스Child.name methodchild,정적Parent.staticName staticparent.해석분기이며객체변환/시간흐름아님. 같은 value 표현식 정적 Parent / 실제 Child 필드 value.name Parent.name field=parent 정적 타입 기준 value.name() Child.name() method=child 실제 객체 기준 value.staticName() Parent.staticName() static=parent 정적 타입 기준 세 줄은 main의 독립 연산 필드/static은 동적 선택 아님
필드·static과 인스턴스 메서드는 선택 기준이 다릅니다 동일value 정적Parent/실제Child;필드Parent.name fieldparent,인스턴스Child.name methodchild,정적Parent.staticName staticparent.해석분기이며객체변환/시간흐름아님. 같은 value 표현식 정적 Parent / 실제 Child 필드 value.name Parent.name field=parent 정적 타입 기준 value.name() Child.name() method=child 실제 객체 기준 value.staticName() Parent.staticName() static=parent 정적 타입 기준 세 줄은 main의 독립 연산 필드/static은 동적 선택 아님
value.name · 필드
정적 타입 Parent에 선언된 name 필드를 선택하여 field=parent를 출력합니다.
value.name() · 인스턴스 메서드
Parent의 계약으로 호출을 컴파일하고 실제 객체의 Child.name()을 실행하여 method=child를 출력합니다.
value.staticName() · 정적 메서드
정적 타입 기준 Parent.staticName()을 선택하여 static=parent를 출력합니다. 인스턴스 동적 디스패치가 아닙니다.

세 결과는 객체가 Parent로 바뀌어서 달라진 것이 아닙니다. 필드 숨김·정적 메서드 숨김과 인스턴스 메서드 오버라이딩은 선택 규칙이 다릅니다.


연습 문제

Post 배열에서 CodePost이면 branch, TextPost이면 bodyLength를 패턴 매칭 instanceof로 출력하세요.

그 밖의 게시글은 general을 출력합니다.

해설 보기
src/PatternDetailReport.java
public final class PatternDetailReport {
    public static void main(String[] args) {
        Post[] values = {
                new CodePost("main"),
                new TextPost(80),
                new Post()
        };
        for (Post value : values) {
            print(value);
        }
    }

    private static void print(Post value) {
        if (value instanceof CodePost codePost) {
            System.out.println("branch=" + codePost.branch);
        } else if (value instanceof TextPost textPost) {
            System.out.println("bodyLength=" + textPost.bodyLength);
        } else {
            System.out.println("general");
        }
    }

    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 {
        private final int bodyLength;
        TextPost(int bodyLength) { this.bodyLength = bodyLength; }
    }
}
branch=main
bodyLength=80
general

패턴 변수는 각 성공 분기 안에서만 유효해 별도 명시적 캐스트가 필요 없습니다.