본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
10장 : 제네릭

제네릭 메서드와 타입 추론

클래스 전체가 제네릭일 필요 없이 특정 메서드만 타입 매개변수를 선언할 수 있습니다.

타입 매개변수 선언은 반환 타입보다 앞에 놓이며 인자와 대입 문맥에서 실제 타입을 추론합니다.

static 메서드는 클래스의 타입 매개변수를 직접 쓸 수 없지만 자기 메서드 타입 매개변수는 가질 수 있습니다.

서로 다른 인자의 타입 관계를 하나의 T로 묶으면 호출 가능한 조합이 제한됩니다.

이 절에서는 메서드 수준에서 타입 정보를 보존합니다.

타입 추론은 단순히 첫 번째 인자의 타입을 복사하지 않습니다. 모든 인자, 메서드의 경계, 결과가 대입되는 목표 타입을 함께 만족하는 타입을 찾습니다. 그래서 서로 다른 숫자를 choose에 넘기면 구체 래퍼 타입 하나가 아니라 공통 상위 타입이 선택될 수 있습니다.


타입 매개변수 선언은 반환 타입 앞에 둔다

static <T> T first(List<T> values)처럼 메서드가 사용할 타입 변수를 먼저 소개합니다.

T의 범위는 해당 메서드 선언입니다.


인자와 목표 타입이 추론에 참여한다

대부분 호출에서는 <String>을 직접 쓰지 않아도 List<String> 인자로 T가 결정됩니다.

모호한 경우 TypeName.<String>method()로 타입 증인을 줄 수 있습니다.

first의 선언에는 String이나 Integer가 한 번도 등장하지 않지만, 호출 인자의 List<T>와 반환 T가 같은 자리임을 선언했기 때문에 결과 타입이 보존됩니다.

GenericMethods.java
import java.util.List;

public class GenericMethods {
    static <T> T first(List<T> values) {
        if (values.isEmpty()) throw new IllegalArgumentException("empty");
        return values.getFirst();
    }

    static <K, V> String pair(K key, V value) {
        return key + "=" + value;
    }

    public static void main(String[] args) {
        String name = first(List.of("Ada", "Linus"));
        Integer number = first(List.of(10, 20));
        System.out.println(name);
        System.out.println(number);
        System.out.println(pair("version", 25));
    }
}

첫 두 줄은 호출마다 T가 각각 String과 Integer로 추론된 결과입니다. 마지막 pair는 K와 V가 독립적이므로 String 키와 Integer 값을 억지로 같은 상위 타입에 맞추지 않습니다.

Ada
10
version=25

first는 호출한 List의 원소 타입을 그대로 반환해 String과 Integer 호출에서 각각 다른 T를 추론합니다.


여러 위치의 같은 T가 관계를 만든다

두 인자와 반환에 같은 T를 쓰면 서로 호환되는 타입만 전달할 수 있습니다.

독립적인 타입이면 <K, V>처럼 변수를 나눕니다.

choose의 한 T와 pair의 독립적인 K, V를 비교하면 같은 타입 변수를 공유한다는 선언의 의미가 드러납니다.

비교 1
static <T> T choose(boolean first, T left, T right) {
    return first ? left : right;
}
비교 2
Number number = choose(true, 1, 2.5); // 공통 상위 타입 추론
String text = GenericMethods.<String>choose(true, "A", "B");
비교 3
// static T broken(T value) { return value; }
// 컴파일 오류: 메서드 앞에 <T> 선언이 없음

choose(true, 1, 2.5)의 결과를 var로 받은 뒤 클래스 이름을 출력해 보세요. 컴파일러가 정한 정적 타입과 실제로 선택된 객체의 런타임 클래스는 같은 개념이 아닙니다. 이어서 두 인자를 모두 String으로 바꾸면 타입 증인 없이도 String으로 좁혀지는 과정을 확인할 수 있습니다.

타입 변수를 여러 개 만든다고 더 정확한 API가 되는 것은 아닙니다. 두 위치가 반드시 같은 타입이어야 할 때만 같은 T를 쓰고, 관계가 없다면 K와 V처럼 분리합니다. 반대로 반환에서 한 번도 쓰지 않는 타입 변수는 와일드카드나 구체 상위 타입으로 더 단순하게 표현할 수 있는지 검토합니다.


제네릭 메서드는 호출마다 타입을 추론하면서 인자와 반환값 사이의 관계를 잃지 않게 합니다.