본문 바로가기

타입 상한과 메서드 추론

제한 없는 T에서 Animal 메서드를 호출할 수 없는 컴파일 실패를 재현하고 extends 상한, 제네릭 타입·메서드의 범위, 타입 추론을 구분합니다.

제네릭 선언의 T에는 기본적으로 어떤 참조 타입이든 들어올 수 있습니다.

따라서 컴파일러는 T를 Object 수준으로만 다룰 수 있고 size()나 sound() 같은 Animal 기능을 허용하지 않습니다.

타입 매개변수 상한은 허용 입력을 줄이는 동시에 구현이 사용할 수 있는 최소 계약을 늘립니다.


제한 없는 타입 매개변수

개발자는 Hospital에 동물만 넣을 생각이지만 선언에는 그 의도가 없습니다.

Hospital<Integer>도 가능한 상태이므로 javac는 T에 size가 있다고 가정할 수 없습니다.

lab/UnboundedAnimalMethodFailure.java
public final class UnboundedAnimalMethodFailure {
    public static void main(String[] args) {
        Hospital<Dog> hospital = new Hospital<>(new Dog("dog", 40));
        System.out.println(hospital.size());
    }

    private static final class Hospital<T> {
        private final T animal;

        Hospital(T animal) {
            this.animal = animal;
        }

        int size() {
            return animal.size();
        }
    }

    private interface Animal {
        int size();
    }

    private record Dog(String name, int size) implements Animal {}
}
javac 핵심 진단
error: cannot find symbol
symbol: method size()
location: variable animal of type T

주석, 변수 이름, main의 실제 Dog 사용은 제네릭 클래스 전체 계약을 바꾸지 않습니다.

클래스는 다른 호출자도 사용할 수 있으므로 선언만 보고 안전해야 합니다.


타입 상한과 허용 동작

<T extends Animal>은 Animal 또는 그 하위 타입만 타입 인자로 허용합니다.

구현 안에서는 T를 Animal로 사용할 수 있고 반환할 때는 실제 T를 유지합니다.

클래스 상속과 인터페이스 구현 모두 제네릭 상한 문법에서는 extends를 씁니다.

src/BoundedAnimalHospital.java
public final class BoundedAnimalHospital {
    public static void main(String[] args) {
        Hospital<Dog> dogs = new Hospital<>(new Dog("d1", 40));
        Dog bigger = dogs.bigger(new Dog("d2", 75));
        dogs.checkup();
        System.out.println("bigger=" + bigger.name());
    }

    private static final class Hospital<T extends Animal> {
        private final T animal;

        Hospital(T animal) {
            this.animal = animal;
        }

        void checkup() {
            System.out.println(animal.name() + ":" + animal.size());
            animal.sound();
        }

        T bigger(T other) {
            return animal.size() >= other.size() ? animal : other;
        }
    }

    private interface Animal {
        String name();

        int size();

        void sound();
    }

    private record Dog(String name, int size) implements Animal {
        public void sound() {
            System.out.println("woof");
        }
    }
}
d1:40
woof
bigger=d2

상한은 T를 Animal로 바꾸는 것이 아닙니다.

구현이 Animal 기능을 안전하게 쓰면서 호출 측의 Dog 타입도 보존합니다.

main은 bigger의 결과 d2를 지역 변수에 받은 다음 dogs.checkup()을 호출합니다. bigger 안에는 animal에 대한 대입이 없으므로 저장한 환자는 여전히 d1입니다. 그래서 출력 순서는 d1:40, woof, bigger=d2입니다. 반환값을 받은 변수와 컨테이너의 보관 값은 같은 역할이 아닙니다.

여러 상한이 필요하면 <T extends BaseClass & FirstRole & SecondRole>처럼 클래스 하나를 먼저, 인터페이스들을 뒤에 둡니다.

상한이 허용하는 멤버와 실제 예제의 대상

bigger가 d2를 지역 결과로 반환해도 checkup은 저장된 d1을 읽습니다. 실제 main 순서에서 반환값과 저장 상태가 달라지는 점을 추적할 수 있습니다.

상한이 허용하는 멤버와 실제 예제의 대상

제한 없는 Hospital의 animal.size 호출은 컴파일 실패한다. Animal 상한이 있는 예제는 저장한 Dog d1을 checkup하고, bigger는 별도로 Dog d2를 반환한다.

상한 계약 · 검진과 반환

상한이 허용하는 멤버와 실제 예제의 대상

Animal 상한은 공통 멤버를 허용하고 Dog 반환을 유지합니다. checkup은 저장한 d1을 검진하며 bigger가 고른 d2로 저장 값이 바뀌지는 않습니다.

저장 환자와 선택 반환 객체의 별개 관계 Hospital<Dog>의 final animal은 d1 40입니다. 점선은 비교가 저장값을 참조하는 관계이며 bigger는 새 d2 75를 반환할 뿐 animal에 대입하지 않습니다. checkup은 d1:40과 woof를 출력하고 main은 뒤 bigger=d2를 출력합니다. 실선은 해당 읽기·선택·출력의 관계이며 객체의 물리 메모리 배치가 아닙니다. Hospital<Dog>의 저장 final animal 참조 Dog d1 / 크기 40 checkup이 읽는 값 d1:40 출력 woof 출력 bigger의 별도 입력 저장 d1 / 40 새 Dog d2 / 75 선택한 반환 객체 40 >= 75는 false Dog d2 반환 main의 지역 변수 bigger=d2 출력 저장 값은 d1 그대로 점선: 비교가 저장값을 참조 · 실선: checkup 읽기와 bigger 반환의 각 관계 main은 비교 결과를 먼저 받고 checkup 다음 bigger=d2 출력 · 저장에 되돌리는 선 없음
저장 환자와 선택 반환 객체의 별개 관계 Hospital<Dog>의 final animal은 d1 40입니다. 점선은 비교가 저장값을 참조하는 관계이며 bigger는 새 d2 75를 반환할 뿐 animal에 대입하지 않습니다. checkup은 d1:40과 woof를 출력하고 main은 뒤 bigger=d2를 출력합니다. 실선은 해당 읽기·선택·출력의 관계이며 객체의 물리 메모리 배치가 아닙니다. Hospital<Dog>의 저장 final animal 참조 Dog d1 / 크기 40 checkup이 읽는 값 d1:40 출력 woof 출력 bigger의 별도 입력 저장 d1 / 40 새 Dog d2 / 75 선택한 반환 객체 40 >= 75는 false Dog d2 반환 main의 지역 변수 bigger=d2 출력 저장 값은 d1 그대로 점선: 비교가 저장값을 참조 실선: 읽기·선택·결과 출력 반환은 animal 대입이 아님
상한이 허용하는 멤버와 실제 예제의 대상
검사·관찰 항목선언이 보장하는 범위원문 결과
제한 없는 타입
Hospital<T>
T에 size() 계약이 없음
main의 Dog 사용만으로 선언의 상한이 바뀌지 않음
animal.size()
컴파일 실패
상한 안의 검진
checkup()
T extends Animal
저장된 d1의 이름·크기·울음 호출
d1:40
woof
상한 안의 반환
bigger(other)
40 >= 75는 거짓
other인 Dog d2 반환
Dog bigger
bigger=d2
제한 없는 타입
Hospital<T>
선언이 보장하는 범위
T에 size() 계약이 없음
main의 Dog 사용만으로 선언의 상한이 바뀌지 않음
원문 결과
animal.size()
컴파일 실패
상한 안의 검진
checkup()
선언이 보장하는 범위
T extends Animal
저장된 d1의 이름·크기·울음 호출
원문 결과
d1:40
woof
상한 안의 반환
bigger(other)
선언이 보장하는 범위
40 >= 75는 거짓
other인 Dog d2 반환
원문 결과
Dog bigger
bigger=d2

main은 bigger의 반환값을 받은 뒤 checkup()을 호출합니다. 저장된 d1이 d2로 바뀌는 것은 아닙니다.

이 예의 조건

  • bigger는 final animal에 대입하지 않는다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
5Dog d1
6크기 40
11저장 d1
1240
13새 Dog d2
1475
원 도표의 설명과 주석
구분내용
전체 읽기 정보상한이 허용하는 멤버와 실제 예제의 대상 상한이 허용하는 멤버와 실제 예제의 대상 제한 없는 Hospital의 animal.size 호출은 컴파일 실패한다. Animal 상한이 있는 예제는 저장한 Dog d1을 checkup하고, bigger는 별도로 Dog d2를 반환한다. 상한 계약 · 검진과 반환 상한이 허용하는 멤버와 실제 예제의 대상 Animal 상한은 공통 멤버를 허용하고 Dog 반환을 유지합니다. checkup은 저장한 d1을 검진하며 bigger가 고른 d2로 저장 값이 바뀌지는 않습니다. 상한이 허용하는 멤버와 실제 예제의 대상 검사·관찰 항목 선언이 보장하는 범위 원문 결과 제한 없는 타입 Hospital<T> T 에 size() 계약이 없음 main 의 Dog 사용만으로 선언의 상한이 바뀌지 않음 animal.size() 컴파일 실패 상한 안의 검진 checkup() T extends Animal 저장된 d1 의 이름·크기·울음 호출 d1:40 woof 상한 안의 반환 bigger(other) 40 >= 75 는 거짓 other 인 Dog d2 반환 Dog bigger bigger=d2 제한 없는 타입 Hospital<T> 선언이 보장하는 범위 T 에 size() 계약이 없음 main 의 Dog 사용만으로 선언의 상한이 바뀌지 않음 원문 결과 animal.size() 컴파일 실패 상한 안의 검진 checkup() 선언이 보장하는 범위 T extends Animal 저장된 d1 의 이름·크기·울음 호출 원문 결과 d1:40 woof 상한 안의 반환 bigger(other) 선언이 보장하는 범위 40 >= 75 는 거짓 other 인 Dog d2 반환 원문 결과 Dog bigger bigger=d2 main 은 bigger 의 반환값을 받은 뒤 checkup() 을 호출합니다. 저장된 d1 이 d2 로 바뀌는 것은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
저장과 후보

저장 d1과 새 d2를 비교합니다.

  • animal: d1/40
  • 후보: d2/75
  • 비교: 40≥75 false
  • 선택 함수가 기존 저장값을 입력으로 읽는다.
반환 보관

main의 bigger만 d2를 받습니다.

  • bigger: d2
  • animal: d1 유지
  • 반환 참조와 저장소 갱신은 다른 동작이다.
실제 관찰 순서

checkup 뒤 main이 bigger를 출력합니다.

  • checkup: d1:40; woof
  • main: bigger=d2
  • 저장된 환자와 선택 결과가 각 호출에서 다르게 보인다.

상한이 허용하는 멤버와 실제 예제의 대상

bigger가 d2를 지역 결과로 반환해도 checkup은 저장된 d1을 읽습니다. 실제 main 순서에서 반환값과 저장 상태가 달라지는 점을 추적할 수 있습니다.

상한이 허용하는 멤버와 실제 예제의 대상

제한 없는 Hospital의 animal.size 호출은 컴파일 실패한다. Animal 상한이 있는 예제는 저장한 Dog d1을 checkup하고, bigger는 별도로 Dog d2를 반환한다.

상한 계약 · 검진과 반환

상한이 허용하는 멤버와 실제 예제의 대상

Animal 상한은 공통 멤버를 허용하고 Dog 반환을 유지합니다. checkup은 저장한 d1을 검진하며 bigger가 고른 d2로 저장 값이 바뀌지는 않습니다.

저장 환자와 선택 반환 객체의 별개 관계 Hospital<Dog>의 final animal은 d1 40입니다. 점선은 비교가 저장값을 참조하는 관계이며 bigger는 새 d2 75를 반환할 뿐 animal에 대입하지 않습니다. checkup은 d1:40과 woof를 출력하고 main은 뒤 bigger=d2를 출력합니다. 실선은 해당 읽기·선택·출력의 관계이며 객체의 물리 메모리 배치가 아닙니다. Hospital<Dog>의 저장 final animal 참조 Dog d1 / 크기 40 checkup이 읽는 값 d1:40 출력 woof 출력 bigger의 별도 입력 저장 d1 / 40 새 Dog d2 / 75 선택한 반환 객체 40 >= 75는 false Dog d2 반환 main의 지역 변수 bigger=d2 출력 저장 값은 d1 그대로 점선: 비교가 저장값을 참조 · 실선: checkup 읽기와 bigger 반환의 각 관계 main은 비교 결과를 먼저 받고 checkup 다음 bigger=d2 출력 · 저장에 되돌리는 선 없음
저장 환자와 선택 반환 객체의 별개 관계 Hospital<Dog>의 final animal은 d1 40입니다. 점선은 비교가 저장값을 참조하는 관계이며 bigger는 새 d2 75를 반환할 뿐 animal에 대입하지 않습니다. checkup은 d1:40과 woof를 출력하고 main은 뒤 bigger=d2를 출력합니다. 실선은 해당 읽기·선택·출력의 관계이며 객체의 물리 메모리 배치가 아닙니다. Hospital<Dog>의 저장 final animal 참조 Dog d1 / 크기 40 checkup이 읽는 값 d1:40 출력 woof 출력 bigger의 별도 입력 저장 d1 / 40 새 Dog d2 / 75 선택한 반환 객체 40 >= 75는 false Dog d2 반환 main의 지역 변수 bigger=d2 출력 저장 값은 d1 그대로 점선: 비교가 저장값을 참조 실선: 읽기·선택·결과 출력 반환은 animal 대입이 아님
상한이 허용하는 멤버와 실제 예제의 대상
검사·관찰 항목선언이 보장하는 범위원문 결과
제한 없는 타입
Hospital<T>
T에 size() 계약이 없음
main의 Dog 사용만으로 선언의 상한이 바뀌지 않음
animal.size()
컴파일 실패
상한 안의 검진
checkup()
T extends Animal
저장된 d1의 이름·크기·울음 호출
d1:40
woof
상한 안의 반환
bigger(other)
40 >= 75는 거짓
other인 Dog d2 반환
Dog bigger
bigger=d2
제한 없는 타입
Hospital<T>
선언이 보장하는 범위
T에 size() 계약이 없음
main의 Dog 사용만으로 선언의 상한이 바뀌지 않음
원문 결과
animal.size()
컴파일 실패
상한 안의 검진
checkup()
선언이 보장하는 범위
T extends Animal
저장된 d1의 이름·크기·울음 호출
원문 결과
d1:40
woof
상한 안의 반환
bigger(other)
선언이 보장하는 범위
40 >= 75는 거짓
other인 Dog d2 반환
원문 결과
Dog bigger
bigger=d2

main은 bigger의 반환값을 받은 뒤 checkup()을 호출합니다. 저장된 d1이 d2로 바뀌는 것은 아닙니다.

이 예의 조건

  • bigger는 final animal에 대입하지 않는다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
5Dog d1
6크기 40
11저장 d1
1240
13새 Dog d2
1475
원 도표의 설명과 주석
구분내용
전체 읽기 정보상한이 허용하는 멤버와 실제 예제의 대상 상한이 허용하는 멤버와 실제 예제의 대상 제한 없는 Hospital의 animal.size 호출은 컴파일 실패한다. Animal 상한이 있는 예제는 저장한 Dog d1을 checkup하고, bigger는 별도로 Dog d2를 반환한다. 상한 계약 · 검진과 반환 상한이 허용하는 멤버와 실제 예제의 대상 Animal 상한은 공통 멤버를 허용하고 Dog 반환을 유지합니다. checkup은 저장한 d1을 검진하며 bigger가 고른 d2로 저장 값이 바뀌지는 않습니다. 상한이 허용하는 멤버와 실제 예제의 대상 검사·관찰 항목 선언이 보장하는 범위 원문 결과 제한 없는 타입 Hospital<T> T 에 size() 계약이 없음 main 의 Dog 사용만으로 선언의 상한이 바뀌지 않음 animal.size() 컴파일 실패 상한 안의 검진 checkup() T extends Animal 저장된 d1 의 이름·크기·울음 호출 d1:40 woof 상한 안의 반환 bigger(other) 40 >= 75 는 거짓 other 인 Dog d2 반환 Dog bigger bigger=d2 제한 없는 타입 Hospital<T> 선언이 보장하는 범위 T 에 size() 계약이 없음 main 의 Dog 사용만으로 선언의 상한이 바뀌지 않음 원문 결과 animal.size() 컴파일 실패 상한 안의 검진 checkup() 선언이 보장하는 범위 T extends Animal 저장된 d1 의 이름·크기·울음 호출 원문 결과 d1:40 woof 상한 안의 반환 bigger(other) 선언이 보장하는 범위 40 >= 75 는 거짓 other 인 Dog d2 반환 원문 결과 Dog bigger bigger=d2 main 은 bigger 의 반환값을 받은 뒤 checkup() 을 호출합니다. 저장된 d1 이 d2 로 바뀌는 것은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
저장과 후보

저장 d1과 새 d2를 비교합니다.

  • animal: d1/40
  • 후보: d2/75
  • 비교: 40≥75 false
  • 선택 함수가 기존 저장값을 입력으로 읽는다.
반환 보관

main의 bigger만 d2를 받습니다.

  • bigger: d2
  • animal: d1 유지
  • 반환 참조와 저장소 갱신은 다른 동작이다.
실제 관찰 순서

checkup 뒤 main이 bigger를 출력합니다.

  • checkup: d1:40; woof
  • main: bigger=d2
  • 저장된 환자와 선택 결과가 각 호출에서 다르게 보인다.

제네릭 메서드의 범위

제네릭 타입의 타입 매개변수는 인스턴스를 만드는 시점에 정해져 필드와 여러 메서드가 공유합니다.

제네릭 메서드의 타입 매개변수는 호출할 때마다 독립적으로 정해집니다.

반환 타입 왼쪽에 <T>를 선언해야 하며 static <T> T identity(T value) 순서로 씁니다.

src/GenericMethodScope.java
public final class GenericMethodScope {
    public static void main(String[] args) {
        Integer viewCount = identity(50);
        String title = identity("제네릭 메서드 질문");
        Double larger = largerNumber(10.5, 8.0);

        System.out.println(title + "=" + viewCount);
        System.out.println("larger=" + larger);
    }

    private static <T> T identity(T value) {
        return value;
    }

    private static <T extends Number & Comparable<T>> T largerNumber(T left, T right) {
        return left.compareTo(right) >= 0 ? left : right;
    }
}
제네릭 메서드 질문=50
larger=10.5

identity의 첫 호출 T는 Integer, 둘째 호출 T는 String입니다.

largerNumber는 Number 기능과 같은 T끼리의 Comparable 계약을 모두 요구합니다.

<Integer>를 호출부에 직접 적을 수도 있지만 인수와 대입 문맥이 충분하면 추론에 맡기는 편이 읽기 쉽습니다.


타입 추론의 입력 문맥

컴파일러는 전달된 인수 타입에서 후보를 모으고 메서드 상한과 목표 타입을 만족하는 타입을 찾습니다.

인수가 서로 다른 하위 타입이면 공통 부모가 추론될 수 있어 반환이 기대보다 넓어질 수 있습니다.

추론 결과가 의도를 숨기면 타입 인자를 명시하거나 메서드 시그니처를 좁힙니다.

src/GenericAnimalMethodInference.java
public final class GenericAnimalMethodInference {
    public static void main(String[] args) {
        Dog first = new Dog("d1", 40, "A");
        Dog second = new Dog("d2", 60, "B");

        Dog bigger = bigger(first, second);
        Animal mixed = bigger(first, new Cat("c1", 80));

        System.out.println("dog-code=" + bigger.code());
        System.out.println("mixed=" + mixed.name());
    }

    private static <T extends Animal> T bigger(T left, T right) {
        return left.size() >= right.size() ? left : right;
    }

    private interface Animal {
        String name();

        int size();
    }

    private record Dog(String name, int size, String code) implements Animal {}

    private record Cat(String name, int size) implements Animal {}
}
dog-code=B
mixed=c1

Dog 두 개에서는 T가 Dog라 고유 code를 쓸 수 있습니다.

Dog와 Cat을 함께 넘긴 이 예제에서는 결과를 공통 Animal 타입의 변수로 받습니다.

“같은 종만 비교”가 요구라면 이런 혼합 호출을 허용하는 메서드 하나로는 충분하지 않습니다.

종을 표현하는 더 정교한 타입 계층이나 각 타입의 인스턴스 메서드가 필요합니다.

메서드 호출과 결과를 받는 타입

identity의 Integer와 String 호출은 독립적이다. Dog 두 개를 비교한 결과는 Dog로, Dog와 Cat을 비교한 결과는 Animal 변수로 받으며 실제로 선택된 값은 Cat c1이다.

호출별 추론 · 선언된 결과

메서드 호출과 결과를 받는 타입

identity 호출은 각각 독립적으로 타입을 정합니다. Dog 두 값은 Dog로 받고 Dog·Cat 비교 결과는 Animal 변수로 받습니다.

호출별 타입 관계와 실제 선택 객체 독립 identity 호출은 Integer 50과 String을 각각 반환합니다. bigger의 두 Dog 호출은 Dog 결과 d2에서 code=B를 읽습니다. Dog와 Cat 호출은 Animal 결과로 받으며 실제 더 큰 객체는 Cat c1입니다. 실선은 해당 호출의 입력·반환·출력 관계이며 상한이 같은 종만 받도록 제한한다는 뜻이 아닙니다. 원문 A · 호출별 T identity 두 호출 Integer 50 String 제네릭 메서드 질문 각 반환 타입 Integer와 String 호출마다 따로 결정 출력 연결 제네릭 메서드 질문=50 원문 B · 두 Dog 인수 두 개 d1 / 40 / A d2 / 60 / B Dog 결과로 받기 40 >= 60은 false d2 선택 Dog 고유 멤버 dog-code=B 원문 B · Dog와 Cat 인수의 공통 상한 Dog d1 / 40 Cat c1 / 80 Animal 결과로 받기 40 >= 80은 false 실제 객체는 Cat 부모 멤버 출력 mixed=c1 실선: 각 호출의 입력·반환·출력 정적 반환 타입과 객체 선택 구별 같은 종만 받는다는 계약 아님
호출별 타입 관계와 실제 선택 객체 독립 identity 호출은 Integer 50과 String을 각각 반환합니다. bigger의 두 Dog 호출은 Dog 결과 d2에서 code=B를 읽습니다. Dog와 Cat 호출은 Animal 결과로 받으며 실제 더 큰 객체는 Cat c1입니다. 실선은 해당 호출의 입력·반환·출력 관계이며 상한이 같은 종만 받도록 제한한다는 뜻이 아닙니다. 원문 A · 호출별 T identity 두 호출 Integer 50 String 제네릭 메서드 질문 각 반환 타입 Integer와 String 호출마다 따로 결정 출력 연결 제네릭 메서드 질문=50 원문 B · 두 Dog 인수 두 개 d1 / 40 / A d2 / 60 / B Dog 결과로 받기 40 >= 60은 false d2 선택 Dog 고유 멤버 dog-code=B 원문 B · Dog와 Cat 인수의 공통 상한 Dog d1 / 40 Cat c1 / 80 Animal 결과로 받기 40 >= 80은 false 실제 객체는 Cat 부모 멤버 출력 mixed=c1 실선: 각 호출의 입력·반환·출력 정적 반환 타입과 객체 선택 구별 같은 종만 받는다는 계약 아님
메서드 호출과 결과를 받는 타입
원문 호출결과를 받는 변수그 변수의 값
identity(50)Integer viewCount50
문자열 identity 호출String title제네릭 메서드 질문
bigger(first, second)
Dog 40 · Dog 60
Dog bigger
d2
code() → B
bigger(first, new Cat(...))
Dog 40 · Cat 80
Animal mixed
실제 값은 Cat c1
name() → c1
identity(50)
결과를 받는 변수
Integer viewCount
그 변수의 값
50
문자열 identity 호출
결과를 받는 변수
String title
그 변수의 값
제네릭 메서드 질문
bigger(first, second)
Dog 40 · Dog 60
결과를 받는 변수
Dog bigger
그 변수의 값
d2
code() → B
bigger(first, new Cat(...))
Dog 40 · Cat 80
결과를 받는 변수
Animal mixed
그 변수의 값
실제 값은 Cat c1
name() → c1

마지막 행은 Animal로 선언된 변수에 결과를 받는 코드입니다. 메서드가 두 인수의 런타임 종을 같게 강제한다는 뜻은 아닙니다.


제네릭 타입과 메서드 선택

타입 관계가 객체 상태와 수명 동안 유지되면 제네릭 타입을 씁니다.

Repository<T>, Box<T>, Hospital<T>처럼 필드와 여러 연산이 같은 T를 공유합니다.

한 호출의 인수와 반환만 연결되면 제네릭 메서드가 더 작고 직접적입니다.

상한은 구현이 실제로 호출할 최소 계약으로 둡니다.

편의를 위해 거대한 부모를 만들면 재사용 가능성이 오히려 줄어듭니다.

Sized, Named처럼 작은 역할 인터페이스를 조합할 수 있습니다.

상한이 여러 개로 복잡해지고 호출자가 긴 타입을 적어야 하면 도메인 전용 추상화를 검토합니다.

메서드의 T 이름이 클래스의 T와 같으면 안쪽 제네릭 메서드 T가 바깥 타입 매개변수를 가립니다.

문법적으로 가능하지만 독자가 같은 타입이라고 오해하기 쉬우므로 R 같은 다른 이름을 사용하거나 메서드 제네릭이 정말 필요한지 확인합니다.

클래스의 T와 호출별 R을 다른 이름으로 드러내면 다음과 같습니다. 한 Converter<String>은 같은 문자열을 보관하지만 각 convert 호출의 결과는 따로 정해집니다.

예시

src/GenericCallScopes.java
import java.util.function.Function;

public final class GenericCallScopes {
    public static void main(String[] args) {
        Converter<String> source = new Converter<>("41");
        Integer number = source.convert(Integer::parseInt);
        String label = source.convert(value -> "id=" + value);
        System.out.println("number=" + number);
        System.out.println("label=" + label);
        System.out.println("source=" + source.value());
    }

    private record Converter<T>(T value) {
        <R> R convert(Function<T, R> operation) {
            return operation.apply(value);
        }
    }
}
number=41
label=id=41
source=41

T는 두 호출에서 String으로 같고 첫 R은 Integer, 둘째 R은 String입니다. 변환 결과를 반환해도 원래 source의 값이나 T를 새 결과 타입으로 바꾸지는 않습니다.


연습 문제

<T extends Scored> T max(T left, T right)를 작성하세요.

ScoredPost 두 개를 넘겨 반환된 객체의 고유 postId를 별도 형변환 없이 직접 사용합니다.

Scored는 score()를 제공하고 ScoredPost는 postId·score 두 값을 갖습니다. null이 아닌 두 객체의 score를 비교해 큰 쪽을 반환하고, 동률이면 left를 반환합니다. 이 연습은 객체의 점수를 바꾸지 않습니다. 입력은 101L/80과 102L/95이며 winner=postId, score=점수 두 줄을 출력합니다.

독립 입력으로 두 번째 점수만 80으로 바꿀 때 반환 postId와 score, T가 ScoredPost로 남는지도 판단하세요.

해설 보기
src/BoundedGenericMethodExercise.java
public final class BoundedGenericMethodExercise {
    public static void main(String[] args) {
        ScoredPost first = new ScoredPost(101L, 80);
        ScoredPost second = new ScoredPost(102L, 95);

        ScoredPost result = max(first, second);
        System.out.println("winner=" + result.postId());
        System.out.println("score=" + result.score());
    }

    private static <T extends Scored> T max(T left, T right) {
        return left.score() >= right.score() ? left : right;
    }

    private interface Scored { int score(); }
    private record ScoredPost(long postId, int score) implements Scored { }
}
winner=102
score=95

max 메서드의 상한은 비교에 필요한 score만 요구하고 T 반환은 ScoredPost의 postId를 보존합니다.

한 호출 안의 입력과 출력 관계가 핵심이므로 제네릭 메서드가 적합합니다.

동률 입력의 반환은 첫 객체라 winner=101, score=80입니다. 비교 결과가 바뀌어도 두 인수와 결과의 ScoredPost 타입 관계는 유지됩니다.