본문 바로가기

다형성과 타입 관계

부모 타입 병원이 Cat을 받아 놓고 Dog로 다운캐스팅해 실패하는 흐름을 재현하고 입력·저장·반환의 동일 타입 관계를 제네릭 계약으로 복원합니다.

Dog와 Cat이 모두 Animal의 자식이라면 AnimalHospital 하나로 중복을 없앨 수 있어 보입니다.

실제로 이 예제의 이름·크기 같은 공통 메서드는 부모 타입으로 호출할 수 있습니다.

하지만 “이 병원에 넣은 구체 타입과 꺼내는 구체 타입이 같다”는 관계는 단순한 부모 타입 하나로 표현되지 않습니다.


잘못된 하위 타입 캐스팅

병원 필드와 메서드가 모두 Animal이므로 어느 동물이든 전달됩니다.

호출자는 개 병원이라고 생각하지만 Cat을 넣는 실수를 컴파일러가 막지 못하고, 반환된 Animal을 Dog로 다운캐스팅할 때 실패합니다.

lab/AnimalHospitalWrongSubtypeFailure.java
public final class AnimalHospitalWrongSubtypeFailure {
    public static void main(String[] args) {
        AnimalHospital dogHospital = new AnimalHospital();
        dogHospital.set(new Dog("dog", 40));
        dogHospital.set(new Cat("cat", 80));

        Dog biggerDog = (Dog) dogHospital.bigger(new Dog("other-dog", 60));
        System.out.println(biggerDog.name());
    }

    private static final class AnimalHospital {
        private Animal animal;
        void set(Animal animal) { this.animal = animal; }
        Animal bigger(Animal other) {
            return animal.size() >= other.size() ? animal : other;
        }
    }

    private interface Animal { String name(); int size(); }
    private record Dog(String name, int size) implements Animal { }
    private record Cat(String name, int size) implements Animal { }
}
실행 결과의 핵심
java.lang.ClassCastException:
class AnimalHospitalWrongSubtypeFailure$Cat cannot be cast to class AnimalHospitalWrongSubtypeFailure$Dog

Animal 다형성은 공통 행동 호출에는 성공했지만 컨테이너의 타입 일관성을 잃었습니다.

입력 시점의 Cat과 실패 시점의 Dog 캐스팅이 멀리 떨어지면 원인을 추적하기 어렵습니다.

두 번째 set이 40짜리 Dog를 80짜리 Cat으로 덮어씁니다. bigger는 그 Cat과 새 Dog 60을 비교해 80 >= 60에 따라 Cat을 반환합니다. 비교 알고리즘은 더 큰 동물을 골랐지만, 뒤의 (Dog)는 Cat 객체를 Dog로 변환하지 못합니다. 실패는 이 검사에서 발생하므로 이름 출력까지 진행하지 않습니다.

반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다

Dog 40을 Cat 80으로 덮어쓴 뒤 Dog 60과 비교해 Cat을 선택하고 마지막 Dog cast에서 실패합니다. 같은 컨테이너의 쓰기·선택·실패를 이어 원인을 보여 줄 수 있습니다.

반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다

Dog40을 Cat80이 덮어쓴다. bigger는 Cat80과 Dog60에서 더 큰 Cat을 정상 선택하지만 호출자의 Dog 캐스팅은 실패하고 출력에는 도달하지 않는다.

실행 순서 · 저장에서 캐스팅까지

반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다

Animal 계약은 Dog 뒤의 Cat 저장을 허용합니다. 더 큰 Cat을 정상 선택한 뒤 호출자의 Dog 캐스팅에서 실패합니다.

덮어쓴 환자와 뒤 캐스팅의 실패 연결 같은 원문에서 Dog 40 저장 뒤 Cat 80이 덮어씁니다. 새 Dog 60과의 80 >= 60 비교는 Cat을 반환합니다. 뒤 Dog 캐스팅은 Cat 객체를 변환하지 못해 ClassCastException이며 이름 출력은 실행하지 않습니다. 실선은 실제 저장·선택·실패의 순서입니다. 원문 · 값과 실패의 순서 첫 set 뒤 상태 Dog / 크기 40 둘째 set 덮어쓰기 Cat / 크기 80 새 Dog 60과 비교 80 >= 60은 true Cat 반환 Dog 캐스팅 실패 ClassCastException 이름 출력에는 못 감 실선: 같은 프로그램의 순서 큰 동물 선택과 Dog 계약은 별개
덮어쓴 환자와 뒤 캐스팅의 실패 연결 같은 원문에서 Dog 40 저장 뒤 Cat 80이 덮어씁니다. 새 Dog 60과의 80 >= 60 비교는 Cat을 반환합니다. 뒤 Dog 캐스팅은 Cat 객체를 변환하지 못해 ClassCastException이며 이름 출력은 실행하지 않습니다. 실선은 실제 저장·선택·실패의 순서입니다. 원문 · 값과 실패의 순서 첫 set 뒤 상태 Dog / 크기 40 둘째 set 덮어쓰기 Cat / 크기 80 새 Dog 60과 비교 80 >= 60은 true Cat 반환 Dog 캐스팅 실패 ClassCastException 이름 출력에는 못 감 실선: 같은 프로그램의 순서 큰 동물 선택과 Dog 계약은 별개
  1. 처음 저장

    실제로 저장하거나 선택한 값
    Dog("dog", 40)를 저장합니다.
    타입과 다음 동작
    set(Animal)이므로 Dog 입력이 허용됩니다.
  2. 다시 저장

    실제로 저장하거나 선택한 값
    Cat("cat", 80)이 앞의 Dog를 덮어씁니다.
    타입과 다음 동작
    set(Animal)은 Cat 입력도 허용합니다.
  3. 크기 비교

    실제로 저장하거나 선택한 값
    저장한 Cat 80과 대상 Dog 60을 비교합니다.
    80 >= 60이 참이어서 Cat을 반환합니다.
    타입과 다음 동작
    bigger의 정적 반환 타입은 Animal.
    실제 반환 객체는 저장했던 Cat입니다.
  4. 호출자의 캐스팅

    실제로 저장하거나 선택한 값
    (Dog)는 이미 반환된 Cat을 검사합니다.
    타입과 다음 동작
    ClassCastException으로 실패합니다.
    뒤의 이름 출력에 도달하지 않습니다.

캐스팅은 객체의 종을 바꾸지 않습니다. 여기서 비교 자체는 더 큰 동물을 골랐지만, 호출자가 요구한 Dog라는 구체 타입 관계는 Animal API가 보장하지 않았습니다.

이 예의 조건

  • 이름 출력은 cast 실패 뒤 미실행이다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
5Dog
6크기 40
8Cat
9크기 80
원 도표의 설명과 주석
구분내용
전체 읽기 정보반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다 반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다 Dog 40을 Cat 80이 덮어쓴다. bigger는 Cat 80과 Dog 60에서 더 큰 Cat을 정상 선택하지만 호출자의 Dog 캐스팅은 실패하고 출력에는 도달하지 않는다. 실행 순서 · 저장에서 캐스팅까지 반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다 Animal 계약은 Dog 뒤의 Cat 저장을 허용합니다. 더 큰 Cat을 정상 선택한 뒤 호출자의 Dog 캐스팅에서 실패합니다. 처음 저장 실제로 저장하거나 선택한 값 Dog("dog", 40) 를 저장합니다. 타입과 다음 동작 set(Animal) 이므로 Dog 입력이 허용됩니다. 다시 저장 실제로 저장하거나 선택한 값 Cat("cat", 80) 이 앞의 Dog를 덮어씁니다. 타입과 다음 동작 set(Animal) 은 Cat 입력도 허용합니다. 크기 비교 실제로 저장하거나 선택한 값 저장한 Cat 80과 대상 Dog 60을 비교합니다. 80 >= 60 이 참이어서 Cat을 반환합니다. 타입과 다음 동작 bigger 의 정적 반환 타입은 Animal . 실제 반환 객체는 저장했던 Cat입니다. 호출자의 캐스팅 실제로 저장하거나 선택한 값 (Dog) 는 이미 반환된 Cat을 검사합니다. 타입과 다음 동작 ClassCastException 으로 실패합니다. 뒤의 이름 출력에 도달하지 않습니다. 캐스팅은 객체의 종을 바꾸지 않습니다. 여기서 비교 자체는 더 큰 동물을 골랐지만, 호출자가 요구한 Dog라는 구체 타입 관계는 Animal API가 보장하지 않았습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
최초 환자

Dog 40을 저장합니다.

  • 저장: Dog 40
  • 넓은 컨테이너 계약은 Dog만 보관하도록 제한하지 않는다.
덮어쓰기와 선택

Cat 80 저장 후 Dog 60과 비교합니다.

  • 저장: Cat 80
  • 비교: 80≥60 true
  • 반환: Cat
  • 업무의 큰 동물 선택이 Dog 타입을 보장하지 않는다.
소비 실패

반환 Cat을 Dog로 캐스팅합니다.

  • 결과: ClassCastException
  • 이름 출력: 미실행
  • 저장 경계의 타입 오염이 나중 소비 지점에서 드러난다.

반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다

Dog 40을 Cat 80으로 덮어쓴 뒤 Dog 60과 비교해 Cat을 선택하고 마지막 Dog cast에서 실패합니다. 같은 컨테이너의 쓰기·선택·실패를 이어 원인을 보여 줄 수 있습니다.

반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다

Dog40을 Cat80이 덮어쓴다. bigger는 Cat80과 Dog60에서 더 큰 Cat을 정상 선택하지만 호출자의 Dog 캐스팅은 실패하고 출력에는 도달하지 않는다.

실행 순서 · 저장에서 캐스팅까지

반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다

Animal 계약은 Dog 뒤의 Cat 저장을 허용합니다. 더 큰 Cat을 정상 선택한 뒤 호출자의 Dog 캐스팅에서 실패합니다.

덮어쓴 환자와 뒤 캐스팅의 실패 연결 같은 원문에서 Dog 40 저장 뒤 Cat 80이 덮어씁니다. 새 Dog 60과의 80 >= 60 비교는 Cat을 반환합니다. 뒤 Dog 캐스팅은 Cat 객체를 변환하지 못해 ClassCastException이며 이름 출력은 실행하지 않습니다. 실선은 실제 저장·선택·실패의 순서입니다. 원문 · 값과 실패의 순서 첫 set 뒤 상태 Dog / 크기 40 둘째 set 덮어쓰기 Cat / 크기 80 새 Dog 60과 비교 80 >= 60은 true Cat 반환 Dog 캐스팅 실패 ClassCastException 이름 출력에는 못 감 실선: 같은 프로그램의 순서 큰 동물 선택과 Dog 계약은 별개
덮어쓴 환자와 뒤 캐스팅의 실패 연결 같은 원문에서 Dog 40 저장 뒤 Cat 80이 덮어씁니다. 새 Dog 60과의 80 >= 60 비교는 Cat을 반환합니다. 뒤 Dog 캐스팅은 Cat 객체를 변환하지 못해 ClassCastException이며 이름 출력은 실행하지 않습니다. 실선은 실제 저장·선택·실패의 순서입니다. 원문 · 값과 실패의 순서 첫 set 뒤 상태 Dog / 크기 40 둘째 set 덮어쓰기 Cat / 크기 80 새 Dog 60과 비교 80 >= 60은 true Cat 반환 Dog 캐스팅 실패 ClassCastException 이름 출력에는 못 감 실선: 같은 프로그램의 순서 큰 동물 선택과 Dog 계약은 별개
  1. 처음 저장

    실제로 저장하거나 선택한 값
    Dog("dog", 40)를 저장합니다.
    타입과 다음 동작
    set(Animal)이므로 Dog 입력이 허용됩니다.
  2. 다시 저장

    실제로 저장하거나 선택한 값
    Cat("cat", 80)이 앞의 Dog를 덮어씁니다.
    타입과 다음 동작
    set(Animal)은 Cat 입력도 허용합니다.
  3. 크기 비교

    실제로 저장하거나 선택한 값
    저장한 Cat 80과 대상 Dog 60을 비교합니다.
    80 >= 60이 참이어서 Cat을 반환합니다.
    타입과 다음 동작
    bigger의 정적 반환 타입은 Animal.
    실제 반환 객체는 저장했던 Cat입니다.
  4. 호출자의 캐스팅

    실제로 저장하거나 선택한 값
    (Dog)는 이미 반환된 Cat을 검사합니다.
    타입과 다음 동작
    ClassCastException으로 실패합니다.
    뒤의 이름 출력에 도달하지 않습니다.

캐스팅은 객체의 종을 바꾸지 않습니다. 여기서 비교 자체는 더 큰 동물을 골랐지만, 호출자가 요구한 Dog라는 구체 타입 관계는 Animal API가 보장하지 않았습니다.

이 예의 조건

  • 이름 출력은 cast 실패 뒤 미실행이다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
5Dog
6크기 40
8Cat
9크기 80
원 도표의 설명과 주석
구분내용
전체 읽기 정보반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다 반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다 Dog 40을 Cat 80이 덮어쓴다. bigger는 Cat 80과 Dog 60에서 더 큰 Cat을 정상 선택하지만 호출자의 Dog 캐스팅은 실패하고 출력에는 도달하지 않는다. 실행 순서 · 저장에서 캐스팅까지 반환된 Cat은 Dog 캐스팅으로 바뀌지 않는다 Animal 계약은 Dog 뒤의 Cat 저장을 허용합니다. 더 큰 Cat을 정상 선택한 뒤 호출자의 Dog 캐스팅에서 실패합니다. 처음 저장 실제로 저장하거나 선택한 값 Dog("dog", 40) 를 저장합니다. 타입과 다음 동작 set(Animal) 이므로 Dog 입력이 허용됩니다. 다시 저장 실제로 저장하거나 선택한 값 Cat("cat", 80) 이 앞의 Dog를 덮어씁니다. 타입과 다음 동작 set(Animal) 은 Cat 입력도 허용합니다. 크기 비교 실제로 저장하거나 선택한 값 저장한 Cat 80과 대상 Dog 60을 비교합니다. 80 >= 60 이 참이어서 Cat을 반환합니다. 타입과 다음 동작 bigger 의 정적 반환 타입은 Animal . 실제 반환 객체는 저장했던 Cat입니다. 호출자의 캐스팅 실제로 저장하거나 선택한 값 (Dog) 는 이미 반환된 Cat을 검사합니다. 타입과 다음 동작 ClassCastException 으로 실패합니다. 뒤의 이름 출력에 도달하지 않습니다. 캐스팅은 객체의 종을 바꾸지 않습니다. 여기서 비교 자체는 더 큰 동물을 골랐지만, 호출자가 요구한 Dog라는 구체 타입 관계는 Animal API가 보장하지 않았습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
최초 환자

Dog 40을 저장합니다.

  • 저장: Dog 40
  • 넓은 컨테이너 계약은 Dog만 보관하도록 제한하지 않는다.
덮어쓰기와 선택

Cat 80 저장 후 Dog 60과 비교합니다.

  • 저장: Cat 80
  • 비교: 80≥60 true
  • 반환: Cat
  • 업무의 큰 동물 선택이 Dog 타입을 보장하지 않는다.
소비 실패

반환 Cat을 Dog로 캐스팅합니다.

  • 결과: ClassCastException
  • 이름 출력: 미실행
  • 저장 경계의 타입 오염이 나중 소비 지점에서 드러난다.

부모 타입 반환의 부담

실수로 다른 종을 넣지 않아도 bigger()의 반환 타입은 Animal입니다.

호출자는 실제로 Dog만 들어 있다는 사실을 코드 밖의 관례로 믿고 캐스팅해야 합니다.

API가 보장해야 할 지식을 소비자의 주석과 기억에 맡긴 셈입니다.

src/PolymorphicHospitalObservation.java
public final class PolymorphicHospitalObservation {
    public static void main(String[] args) {
        AnimalHospital hospital = new AnimalHospital(new Dog("d1", 30));
        Animal result = hospital.bigger(new Dog("d2", 70));

        System.out.println("static-type=Animal");
        System.out.println("runtime-type=" + result.getClass().getSimpleName());
        System.out.println("name=" + result.name());
    }

    private record AnimalHospital(Animal patient) {
        Animal bigger(Animal other) {
            return patient.size() >= other.size() ? patient : other;
        }
    }

    private interface Animal { String name(); int size(); }
    private record Dog(String name, int size) implements Animal { }
}
static-type=Animal
runtime-type=Dog
name=d2

부모 기능만 필요하다면 이 반환도 충분합니다.

그러나 Dog 고유 메서드를 이어서 호출해야 한다면 캐스팅이 필요합니다.

구체 타입을 보존하는 요구가 있는지 먼저 확인해야 합니다.


Class 토큰의 런타임 검사

병원 생성 시 허용 타입의 Class를 받아 입력을 검사할 수 있습니다.

잘못된 값은 set 지점에서 빠르게 거부하지만 여전히 실행 중 검사이고, 반환 시 cast 로직도 병원 내부에 남습니다.

src/RuntimeCheckedAnimalHospital.java
public final class RuntimeCheckedAnimalHospital {
    public static void main(String[] args) {
        CheckedHospital<Dog> dogs = new CheckedHospital<>(Dog.class);
        dogs.set(new Dog("dog", 50));
        System.out.println("stored=" + dogs.get().name());

        try {
            dogs.setUntyped(new Cat("cat", 30));
        } catch (IllegalArgumentException error) {
            System.out.println("rejected=" + error.getMessage());
        }
    }

    private static final class CheckedHospital<T extends Animal> {
        private final Class<T> type;
        private Animal value;

        CheckedHospital(Class<T> type) { this.type = type; }
        void set(T value) { this.value = value; }

        void setUntyped(Animal value) {
            if (!type.isInstance(value)) {
                throw new IllegalArgumentException("expected " + type.getSimpleName());
            }
            this.value = value;
        }

        T get() { return type.cast(value); }
    }

    private interface Animal { String name(); int size(); }
    private record Dog(String name, int size) implements Animal { }
    private record Cat(String name, int size) implements Animal { }
}
stored=dog
rejected=expected Dog

Class 토큰은 역직렬화나 외부 플러그인처럼 런타임 타입 검사가 본질인 경계에서 유용합니다.

일반 소스 코드의 타입 관계를 표현하려고 매번 토큰과 cast를 쓰는 것은 컴파일러가 할 일을 수동으로 반복합니다.


제네릭의 타입 관계 보존

Hospital<T>의 필드, set 인수, bigger 인수, 반환을 모두 T로 묶으면 생성 시 선택한 타입이 전체 API에 유지됩니다.

Animal 상한을 두면 공통 size도 호출할 수 있습니다.

src/TypePreservingAnimalHospital.java
public final class TypePreservingAnimalHospital {
    public static void main(String[] args) {
        Hospital<Dog> dogs = new Hospital<>();
        dogs.set(new Dog("d1", 35, "herding"));
        Dog bigger = dogs.bigger(new Dog("d2", 65, "retriever"));

        System.out.println("name=" + bigger.name());
        System.out.println("breed=" + bigger.breed());
    }

    private static final class Hospital<T extends Animal> {
        private T animal;
        void set(T animal) { this.animal = animal; }
        T bigger(T other) { return animal.size() >= other.size() ? animal : other; }
    }

    private interface Animal { String name(); int size(); }
    private record Dog(String name, int size, String breed) implements Animal { }
}
name=d2
breed=retriever

반환 정적 타입이 Dog이므로 고유한 breed도 캐스팅 없이 호출합니다.

이 Hospital<Dog>에 Cat을 set하거나 bigger의 인수로 전달하는 호출은 컴파일 단계에서 거부됩니다.

다형성은 공통 행동의 상한을 제공하고 제네릭은 같은 T 관계를 보존합니다.

두 기능은 경쟁이 아니라 결합 관계입니다.

부모 타입·Class 검사·Dog 계약은 어디서 작동하는가

부모 타입 비교, Class 토큰을 쓰는 Dog 병원, Dog로 지정한 제네릭 병원의 입력 검사와 실제 결과를 비교한다. 토큰 병원의 넓은 입력은 Animal이며 Object 수신이나 생성 factory는 없다.

타입 경계 · 컴파일과 실행

부모 타입·Class 검사·Dog 계약은 어디서 작동하는가

부모 Animal API, Class 토큰 병원, Hospital<Dog>의 계약을 비교합니다. 세 프로그램의 관찰이며 토큰 검사와 컴파일 계약은 구분합니다.

부모 관찰·Class 검사·T 반환의 역할 서로 다른 세 원문의 계약입니다. Animal 반환 선언은 실제 Dog 선택과 별개이며 부모 API로 이름을 읽습니다. Dog 토큰은 setUntyped의 Cat을 런타임에 거부합니다. Hospital<Dog>의 T 반환은 Dog 고유 breed를 읽게 합니다. 실선은 각 프로그램의 실제 입력·검사·반환·출력이며 영역 사이의 순차 개선 실행이 아닙니다. 원문 A · 부모 API 환자 비교 d1 / 30, d2 / 70 반환 선언 Animal 실제 선택 객체는 Dog 이름 관찰 static-type=Animal runtime-type=Dog name=d2 원문 B · Class 검사 Dog 토큰으로 저장 set(Dog) 정상 stored=dog setUntyped에 Cat isInstance는 false 예외 처리 출력 rejected=expected Dog 원문 C · T 반환 Hospital<Dog> 비교 d1 / 35, d2 / 65 T는 Dog로 유지 선택은 d2 고유 멤버 출력 name=d2 breed=retriever 각 영역은 서로 다른 원문 Class 검사와 정적 T 계약 구별 실선: 실제 선택·검사·출력
부모 관찰·Class 검사·T 반환의 역할 서로 다른 세 원문의 계약입니다. Animal 반환 선언은 실제 Dog 선택과 별개이며 부모 API로 이름을 읽습니다. Dog 토큰은 setUntyped의 Cat을 런타임에 거부합니다. Hospital<Dog>의 T 반환은 Dog 고유 breed를 읽게 합니다. 실선은 각 프로그램의 실제 입력·검사·반환·출력이며 영역 사이의 순차 개선 실행이 아닙니다. 원문 A · 부모 API 환자 비교 d1 / 30, d2 / 70 반환 선언 Animal 실제 선택 객체는 Dog 이름 관찰 static-type=Animal runtime-type=Dog name=d2 원문 B · Class 검사 Dog 토큰으로 저장 set(Dog) 정상 stored=dog setUntyped에 Cat isInstance는 false 예외 처리 출력 rejected=expected Dog 원문 C · T 반환 Hospital<Dog> 비교 d1 / 35, d2 / 65 T는 Dog로 유지 선택은 d2 고유 멤버 출력 name=d2 breed=retriever 각 영역은 서로 다른 원문 Class 검사와 정적 T 계약 구별 실선: 실제 선택·검사·출력
부모 타입·Class 검사·Dog 계약은 어디서 작동하는가
원문 API입력·반환의 경계실제 호출에서 관찰한 것
부모 타입 비교AnimalHospital.bigger(Animal)은 Animal을 반환합니다.d1(30)과 d2(70) 중 d2를 선택합니다.
변수의 정적 타입: Animal
실제 객체: Dog
토큰 병원의 일반 입출력CheckedHospital<Dog>의 set(T)는 Dog 계약입니다.
get()은 저장된 Animal을 Dog.class의 cast로 검사해 반환합니다.
Dog를 저장한 뒤 읽습니다.
stored=dog
토큰 병원의 넓은 입력setUntyped(Animal)은 Cat 인수를 컴파일 시 허용합니다.
Dog.class.isInstance가 실행 중 거짓을 반환합니다.
저장 전에 IllegalArgumentException을 던지고 main이 잡습니다.
rejected=expected Dog
Dog로 지정한 제네릭 비교Hospital<Dog>의 저장·비교 인수·반환에 Dog 계약이 적용됩니다.d1(35)과 d2(65) 중 d2를 선택합니다.
name=d2
breed=retriever
breed()를 명시적 캐스팅 없이 호출합니다.
부모 타입 비교
입력·반환의 경계
AnimalHospital.bigger(Animal)은 Animal을 반환합니다.
실제 호출에서 관찰한 것
d1(30)과 d2(70) 중 d2를 선택합니다.
변수의 정적 타입: Animal
실제 객체: Dog
토큰 병원의 일반 입출력
입력·반환의 경계
CheckedHospital<Dog>의 set(T)는 Dog 계약입니다.
get()은 저장된 Animal을 Dog.class의 cast로 검사해 반환합니다.
실제 호출에서 관찰한 것
Dog를 저장한 뒤 읽습니다.
stored=dog
토큰 병원의 넓은 입력
입력·반환의 경계
setUntyped(Animal)은 Cat 인수를 컴파일 시 허용합니다.
Dog.class.isInstance가 실행 중 거짓을 반환합니다.
실제 호출에서 관찰한 것
저장 전에 IllegalArgumentException을 던지고 main이 잡습니다.
rejected=expected Dog
Dog로 지정한 제네릭 비교
입력·반환의 경계
Hospital<Dog>의 저장·비교 인수·반환에 Dog 계약이 적용됩니다.
실제 호출에서 관찰한 것
d1(35)과 d2(65) 중 d2를 선택합니다.
name=d2
breed=retriever
breed()를 명시적 캐스팅 없이 호출합니다.

부모 타입 예제의 static-type=Animal은 소스의 변수 선언을 설명하는 출력 문자열이며 런타임 반사 결과가 아닙니다. 토큰 예제는 Animal 입력을 검사하며 Object 수신이나 factory 생성은 없습니다. Hospital<Animal>을 선택하면 여러 Animal 하위 타입을 담을 수 있습니다.


다형성 도구의 선택 기준

여러 종을 한 컬렉션에 섞어 공통 행동만 호출하면 List<Animal> 같은 부모 타입이 적합합니다.

선택한 타입 인자를 입력과 반환에 함께 적용하려면 제네릭이 적합합니다. Hospital<Dog>는 Dog 계약을 유지하고, Hospital<Animal>을 선택하면 여러 Animal 하위 타입을 담을 수 있습니다.

외부 데이터가 어떤 타입인지 실행 중 판별하고 객체를 만들어야 하면 Class 토큰이나 명시적 타입 식별자가 필요합니다.

제네릭을 사용해도 런타임 데이터가 신뢰할 수 있다는 뜻은 아닙니다.

JSON의 종류 필드, DB 행, 네트워크 메시지는 입력 경계에서 검증해야 합니다.

검증 뒤 내부에서는 Hospital<Dog>처럼 안전한 타입으로 바꿔 캐스팅을 반복하지 않습니다.

공통 부모에 모든 구체 기능을 올려놓아 상한을 편하게 만들지 않습니다.

이 예제의 Animal은 공통 이름·크기 계약만 제공하고 Dog의 breed 같은 정보는 Dog에 남깁니다.

상한이 비대해지면 다른 구현이 의미 없는 메서드를 구현하게 됩니다.


연습 문제

Arena<T extends Competitor>를 만들고 현재 참가자와 도전자의 score를 비교해 T를 반환하세요.

Arena<Robot>의 결과에서 Robot 고유 serial을 캐스팅 없이 출력합니다.

Competitor는 name()과 score()를 제공합니다. Robot의 값은 name·score·serial 순서입니다. Arena는 생성자에서 현재 참가자를 받아 보관하며 winner(challenger)는 점수가 큰 쪽을 반환합니다. 동률이면 현재 참가자를 유지하고, 반환만으로 보관된 참가자를 갱신하지 않습니다. 두 인수는 null이 아닌 것으로 정합니다.

현재 참가자 R1/70/1001과 도전자 R2/90/1002를 사용해 winner=이름, serial=번호 두 줄을 출력합니다. 독립 조건에서 도전자를 R3/70/1003으로 바꾸면 선택 이름과 serial은 각각 무엇인가요?

해설 보기
src/TypeRelationshipExercise.java
public final class TypeRelationshipExercise {
    public static void main(String[] args) {
        Arena<Robot> arena = new Arena<>(new Robot("R1", 70, 1001));
        Robot winner = arena.winner(new Robot("R2", 90, 1002));
        System.out.println("winner=" + winner.name());
        System.out.println("serial=" + winner.serial());
    }

    private static final class Arena<T extends Competitor> {
        private final T current;
        Arena(T current) { this.current = current; }
        T winner(T challenger) {
            return current.score() >= challenger.score() ? current : challenger;
        }
    }

    private interface Competitor { String name(); int score(); }
    private record Robot(String name, int score, int serial) implements Competitor { }
}
winner=R2
serial=1002

상한은 비교에 필요한 score만 제공하고 T 반환은 Robot의 구체 정보를 유지합니다.

독립 동률 조건에서는 70 >= 70이 참이므로 R1과 serial 1001이 나옵니다. 반환 정적 타입을 Robot으로 유지하므로 serial()을 호출할 수 있으며, serial 크기가 승패를 결정한 것은 아닙니다.

이 Arena<Robot>에는 Robot 계약에 맞는 값만 전달할 수 있습니다. Arena<Competitor>를 선택한다면 여러 Competitor 하위 타입을 함께 비교할 수 있습니다.