타입 추론과 제네릭 메서드
타입 추론 은 각 메서드 호출과 해당 선언을 살펴보고 해당 호출을 적용할 수 있는 타입 인자(또는 인수)를 결정하는 Java 컴파일러의 기능입니다. 추론 알고리즘은 인수의 타입과 가능한 경우 결과가 할당되거나 반환되는 타입을 결정합니다. 마지막으로 추론 알고리즘은 모든 인수를 사용할 수 있는 가장 구체적인 타입을 찾으려고 시도합니다.
이 마지막 요점을 설명하기 위해 다음 예제에서는 추론을 통해 선택 메서드에 전달되는 두 번째 인수가 Serializable 타입임을 확인합니다:
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());제네릭 메서드에서는 일반 메서드처럼 꺾쇠 괄호 사이에 타입을 지정하지 않고도 제네릭 메서드를 호출할 수 있는 타입 추론에 대해 소개했습니다. Box 클래스가 필요한 다음 예제인 BoxDemo를 살펴보겠습니다:
public class BoxDemo {
public static <U> void addBox(U u,
java.util.List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
new java.util.ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}다음은 이 예제의 출력입니다:
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]제네릭 메서드 addBox()는 U라는 하나의 타입 매개변수를 정의합니다. 일반적으로 Java 컴파일러는 제네릭 메서드 호출의 타입 매개변수를 유추할 수 있습니다. 따라서 대부분의 경우 유형 매개변수를 지정할 필요가 없습니다. 예를 들어, 제네릭 메서드 addBox()를 호출하려면 다음과 같이 타입 감시를 사용하여 타입 매개변수를 지정할 수 있습니다:
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);또는 타입 감시를 생략하면 Java 컴파일러는 메서드의 인수를 통해 자동으로 타입 매개변수가 Integer라고 추론합니다:
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
제네릭 클래스의 타입 추론 및 인스턴스화
컴파일러가 컨텍스트에서 타입 인자를 유추할 수 있는 한, 제네릭 클래스의 생성자를 호출하는 데 필요한 타입 인자를 빈 타입 매개변수 집합(<>)으로 대체할 수 있습니다. 이 대괄호 쌍을 비공식적으로 다이아몬드라고 부릅니다.
예를 들어 다음 변수 선언을 생각해 보세요:
Map<String, List<String>> myMap = new HashMap<String, List<String>>();생성자의 매개변수화된 타입을 빈 타입 매개변수 집합(<>)으로 대체할 수 있습니다:
Map<String, List<String>> myMap = new HashMap<>();제네릭 클래스 인스턴스화 중에 타입 추론을 활용하려면 다이아몬드를 사용해야 합니다. 다음 예제에서 컴파일러는 HashMap() 생성자가 HashMap 원시 타입을 참조하지 않고 Map<String, List<String>> 타입을 참조하기 때문에 확인되지 않은 변환 경고를 생성합니다:
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
제네릭 및 비 제네릭 클래스의 타입 추론과 제네릭 생성자
생성자는 제네릭 클래스와 비 제네릭 클래스 모두에서 제네릭(즉, 자체적인 형식적 타입 매개변수를 선언하는 것)일 수 있다는 점에 유의하세요. 다음 예를 살펴보세요:
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}MyClass 클래스의 다음 인스턴스화를 생각해 봅시다:
new MyClass<Integer>("")이 문은 매개변수화된 타입 MyClass<Integer>;의 인스턴스를 생성합니다. 이 문은 제네릭 class MyClass<X>의 형식적 타입 매개변수 X에 대해 명시적으로 Integer 타입을 지정합니다. 이 제네릭 클래스의 생성자에는 형식적 타입 매개변수인 T가 포함되어 있습니다. 컴파일러는 이 제네릭 클래스의 생성자의 형식 형식 매개변수 T에 대해 String 타입을 유추합니다(이 생성자의 실제 매개변수는 String 객체이기 때문입니다).
Java SE 7 이전 릴리스의 컴파일러는 제네릭 메서드와 유사하게 제네릭 생성자의 실제 타입 매개변수를 유추할 수 있습니다. 그러나 Java SE 7 이상의 컴파일러는 다이아몬드(<>)를 사용하면 인스턴스화되는 제네릭 클래스의 실제 타입 매개변수를 유추할 수 있습니다. 다음 예제를 살펴보겠습니다:
MyClass<Integer> myObject = new MyClass<>("");이 예제에서 컴파일러는 제네릭 클래스 MyClass<X>의 형식 형식 매개변수 X에 대해 Integer 타입을 추론합니다. 이 제네릭 클래스의 생성자의 형식적 타입 매개변수 T에 대해 String 타입을 유추합니다.
Note: 추론 알고리즘은 호출 인수, 대상 유형 및 예상되는 명백한 반환 유형만을 사용하여 유형을 추론한다는 점에 유의해야 합니다. 추론 알고리즘은 프로그램 후반부의 결과에는 사용하지 않습니다.
타겟 타입
Java 컴파일러는 타 타입을 활용하여 일반 메서드 호출의 타입 매개변수를 유추합니다. 표현식의 대상 타입은 표현식이 나타나는 위치에 따라 Java 컴파일러가 예상하는 데이터 타입입니다. 다음과 같이 선언된 메서드 Collections.emptyList()를 예로 들어 보겠습니다:
static <T> List<T> emptyList();다음 할당문을 고려하세요:
List<String> listOne = Collections.emptyList();이 문은 이 데이터 타입이 대상 타입인 List<String>의 인스턴스를 기대하고 있습니다. 메서드 emptyList()는 List<T> 타입의 값을 반환하므로 컴파일러는 타입 인수 T가 String 값이어야 한다고 유추합니다. 이는 Java SE 7과 8 모두에서 작동합니다. 또는 타입 감시를 사용하여 다음과 같이 T의 값을 지정할 수 있습니다:
List<String> listOne = Collections.<String>emptyList();그러나 이 맥락에서는 필요하지 않습니다. 하지만 다른 상황에서는 필요합니다. 다음 방법을 고려해 보세요:
void processStringList(List<String> stringList) {
// process stringList
}빈 목록으로 processStringList() 메서드를 호출하고 싶다고 가정해 보겠습니다. Java SE 7에서는 다음 문이 컴파일되지 않습니다:
processStringList(Collections.emptyList());Java SE 7 컴파일러는 다음과 유사한 오류 메시지를 생성합니다:
List<Object> cannot be converted to List<String>컴파일러는 타입 인수 T에 대한 값이 필요하므로 Object 값으로 시작합니다. 따라서 Collections.emptyList()를 호출하면 processStringList() 메서드와 호환되지 않는 List<Object> 타입의 값을 반환하므로 Java SE 7에서는 다음과 같이 타입 인자의 값을 지정해야 합니다:
processStringList(Collections.<String>emptyList());Java SE 8에서는 더 이상 필요하지 않습니다. 대상 타입에 대한 개념이 메서드 인자(예: processStringList()의 인자)를 포함하도록 확장되었습니다. 이 경우 processStringList()에는 List<String> 타입의 인수가 필요합니다. Collections.emptyList() 메서드는 List<T> 값을 반환하므로, 컴파일러는 List<String>의 대상 타입을 사용하여 타입 인수 T가 String 값을 갖는다고 유추합니다. 따라서 Java SE 8에서는 다음 문이 컴파일됩니다:
processStringList(Collections.emptyList());
람다 표현식에서 타겟 타입 지정하기
다음과 같은 메서드가 있다고 가정해 보겠습니다:
public static void printPersons(List<Person> roster, CheckPerson tester)그리고
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester) 그런 다음 다음 코드를 작성하여 이러한 메서드를 호출합니다:
printPersons(
people,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25);그리고
printPersonsWithPredicate(
people,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25);)이 경우 람다 표현식의 타입은 어떻게 확인할 수 있을까요?
Java 런타임이 printPersons() 메서드를 호출할 때 데이터 타입이 CheckPerson이므로 람다 식은 이 타입입니다. 그러나 Java 런타임이 printPersonsWithPredicate() 메서드를 호출할 때는 데이터 타입이 Predicate<Person>이므로 람다 식은 이 타입입니다. 이러한 메서드가 예상하는 데이터 타입을 대상 타입이라고 합니다. Java 컴파일러는 람다 식의 타입을 결정하기 위해 람다 식이 발견된 컨텍스트 또는 상황의 타겟겟 타입을 사용합니다. 따라서 Java 컴파일러가 대상 타입을 결정할 수 있는 상황에서만 람다 표현식을 사용할 수 있습니다.:
- 변수 선언
- 할당
- return 문
- 배열 초기화자
- 메서드 또는 생성자 인자
- 람다 표현식 본문
- 조건 표현식,
?: - 형 변환 표현식
대상 타입과 메서드 인자
메서드 인자의 경우 Java 컴파일러는 오버로드 해결과 타입 인자 추론이라는 두 가지 다른 언어 기능을 사용하여 대상 타입을 결정합니다.
다음 두 가지 함수 인터페이스(java.lang.Runnable 및 java.util.concurrent.Callable<V>를 고려하세요:
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}Runnable.run() 메서드는 값을 반환하지 않지만, Callable<V>.call() 메서드는 값을 반환합니다.
다음과 같이 메서드 호출을 오버로드했다고 가정해 보겠습니다(메서드 오버로드에 대한 자세한 내용은 메서드 정의하기 섹션을 참조하세요):
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}다음 문에서 호출되는 메서드는 무엇인가요?
String s = invoke(() -> "done");이 메서드는 값을 반환하므로 invoke(Callable<T>) 메서드가 호출되고, invoke(Runnable) 메서드는 호출되지 않습니다. 이 경우 람다 표현식 () -> "done"의 타입은 Callable<T>입니다.