본문으로 건너뛰기

안동민 개발노트

본문 시작

instanceof와 오버라이딩

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

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

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

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

instanceof는 null과 하위 타입까지 포함해 호환성을 묻는다

왼쪽 참조를 오른쪽 타입으로 안전하게 캐스팅할 수 있을 때만 true이며 null은 어떤 객체 타입과도 호환되지 않습니다.

실제 값CodePost와 관계결과
new CodePost()직접 호환true
CodePost 하위 객체상속 호환true
new TextPost()형제 타입false
null객체 없음false

@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

매개변수 없는 부모 publishint 매개변수가 있는 자식 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이므로 별도 캐스팅 예외가 나지 않습니다.

패턴 매칭은 검사와 자식 변수 생성을 한 흐름으로 합친다

instanceof 패턴은 호환성 검사와 타입이 보장된 지역 변수 생성을 한 제어 흐름으로 연결합니다.

  1. 1
    부모 참조

    post

  2. 2
    패턴 검사

    instanceof CodePost codePost

  3. 3
    true 경로

    codePost 사용 가능

  4. 4
    false 경로

    codePost 없음

  5. 5
    합류

    스코프 분석


패턴 매칭

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

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

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

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


동적 메서드 탐색

오버라이딩과 오버로딩은 선택 시점이 다르다

같은 이름이라도 시그니처가 같으면 실제 객체가, 매개변수가 다르면 컴파일 시 정적 타입이 대상을 고릅니다.

  1. override

    같은 시그니처 · 실행 시 실제 객체 구현 선택

  2. overload

    다른 매개변수 · 컴파일 시 정적 타입으로 선택

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 객체의 오버라이딩을 선택합니다.

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

멤버 종류에 따라 다형적 선택 여부가 갈린다

인스턴스 메서드만 실제 객체의 override를 우선하고 필드·static은 참조 변수의 정적 타입을 따릅니다.

멤버선택 기준Child를 Parent로 볼 때
인스턴스 메서드실제 객체Child 구현
필드정적 타입Parent 필드
static 메서드정적 타입Parent static
private 메서드상속 계약 아님각 클래스 별도

연습 문제

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

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