안동민 개발노트

본문 시작

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라는 보장이 없으므로 사용할 수 없습니다.

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

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

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

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

PatternMatchingInstanceof의 CodePost 입력은 성공 블록에서 codePost를 사용하고 반환한다. Post 입력은 블록을 건너뛰고 general을 출력하며 이 위치에 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을 출력하고 메서드가 끝납니다.
  1. new CodePost("feature/login") · 성공

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

  2. new Post() · 실패

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

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


동적 메서드 탐색

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 참조의 필드·정적 메서드와 Child 인스턴스 메서드

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

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

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

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