https://sundaland.tistory.com/161
[ ▶ Type Inference ]
타입 유추는 메서드 호출에 적용할 수 있는 타입 아규먼트 (또는 아규먼트)를 결정하기 위해 해당 메서드 호출 및 해당 메서드 선언을 살펴보는 자바 컴파일러의 기능이다.
추론 알고리즘은 타입 아규먼트와 (가능한 경우) 리턴되는 타입을 결정한다.
추론 알고리즘은 모든 아규먼트와 함께 동작하는 특정 타입을 찾으려고 시도한다.
아래 코드의 타입 추론은 pick 메서드에 전달되는 두 번째 아규먼트가 Serializalbe 유형임을 확인한다.
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
[ ▷ 타입 유추와 제너릭 메서드 ]
제너릭 메서드는 꺽쇠 괄호 사이에 타입을 지정하지 않고 일반 메서드처럼 일반 메서드를 호출할 수 있는 타입 유추를 도입했다.
List<String> list = new ArrayList<>();
list.add("korea");
list.add("usa");
list.add("england");
list.add("japan");
String[] strs = new String[list.size()];
String[] ret = list.toArray(strs);
// String[] ret = list.<String>toArray(strs); // 컴파일러 유추를 위해 <String>을 생략
다음 코드는 Box 클래스가 필요하다.
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라는 하나의 타입 파라미터를 정의한다.
일반적으로 자바 컴파일러는 제너릭 호출의 타입 파라미터를 유추할 수 있다. 따라서 대부분의 경우 이를 지정할 필요가 없다.
예를 들어 제너릭 메서드 addBox를 호출하려면 다음과 같이 타입 감시를 사용(addBox 메서드 이름 앞에 <Integer>)하여 타입 파라미터를 지정할 수 있다.
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
타입 감시를 생략하면 자바 컴파일러가 타입 파라미터가 Integer라고 자동으로 추론한다, (메소드 아규먼트에서 추론)
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
[ ▷ Type Inference and Instantiation of Generic Classes ]
컴파일러가 컨텍스트에서 타입 아규먼트를 유추할 수 있는 한 제너릭 클래스의 생성자를 호출하는 데 필요한 타입 아규먼트를 빈 타입 파라미터 세트 (< >)로 바꿀 수 있다. 앞에서 말했듯이 비공식적으로 다이아몬드라고 한다,
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
생성자의 파라미터화된 타입을 텅빈 타입 파라미터 세트(< >)로 대체할 수 있다.
Map<String, List<String>> myMap = new HashMap<>();
제너릭 클래스 인스턴스화 중에 타입 유추를 활용하려면 다이아몬드를 사용해야 한다.
아래의 예제에서 컴파일러는 HashMap() 생성자가 Map<String, List<String>> 타입이 아닌 HashMap 원시(Raw) 타입을 참조하기 때문에 확인되지 않은(Unchecked) 변환 경고를 생성한다.
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
[ ▷ Type Inference and Generic Constructors of Generic and Non-Generic Classes ]
생성자는 제너릭 클래스와 제너릭이 아닌 클래스 모두에게, 제너릭 클래스 생성자로 정의할 수 있다.
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
new MyClass<Integer>("")
위 코드는 파라미터화된 타입 MyClass<Integer>의 인스턴스를 작성한다. 이 코드는 제너릭 클래스 MyClass<X>의 formal 타입 파라미터 X에 대해 Integer 타입을 명시적으로 지정한다.
이 제너릭 클래스의 생성자에는 formal 타입 파라미터 T가 포함(제너릭 클래스 생성자)되어 있다.
컴파일러는 이 제너릭 클래스 생성자의 formal 타입 파라미터 T에 대해 String 타입을 유추한다. (이 생성자의 실제 타입 파라미터는 String 객체이다.)
자바 SE 7 이전 릴리스의 컴파일러는 제너릭 메서드와 유사하게 제너릭 생성자의 실제 타입 파라미터를 유추할 수 있다.
자바 SE 7 이상의 컴파일러는 다이아몬드를 사용하는 경우 인스턴스화되는 제너릭 클래스의 실제 타입 파라미터를 유추할 수 있다.
MyClass<Integer> myObject = new MyClass<>("");
이 예제에서 컴파일러는 제너릭 클래스 MyClass<X>의 formal 타입 파라미터 X에 대해 Integer 타입을 유추한다.
컴파일러는 이 제너릭 클래스 생성자의 formal 타입 파라미터 T에 대한 String 타입을 유추한다,
# 유추 알고리즘은 호출 아규먼트, 타겟 타입 및 가능하고 명백한 예상 리턴 타입만 사용하여 타입 유추한다는 점에 유의하는 것이 중요하다.
추론 알고리즘은 프로그램 후반부의 결과를 사용하지 않는다.
[ ▷ 타겟 타입 ]
자바 컴파일러는 제너릭 메소드 호출의 타입 파라미터를 유추하기 위해 타겟 타입 지정을 이용한다.
표현식의 타겟 타입은 표현식이 나타나는 위치에 따라 자바 컴파일러가 예산하는 데이터 타입이다.
다음은 Colletcions.emptyList 메서드와 할당 명령문이다.
state <T> List<T> emptyList();
List<String> listOne = Collections.emptyList();
위 코드는 List<String> 인스턴스를 기대하고 있다. 이 데이터 타입은 타겟 타입이다. emptryList 메서드는 List<T> 타입의 값을 반환하기 때문에 컴파일러는 타입 아규먼트 T가 String 값이어야 한다고 추론한다.
이는 자바 SE 7 및 8 모두에서 작동한다.또는 타입 감시( .<String> )을 사용하고 다음과 같이 T 값을 지정할 수 있다.
List<String> listOne = Collections.<String>emptyList();
하지만 이 코드에 타입 감시가 필요하지 않다. 하지만 다음과 같은 상황에는 필요로 한다.
void processStringList(List<String> stringList) {
// process stringList
}
emptyListt 메서드 호출로 processStringList 메서드에게 아규먼트를 전달하는 메서드를 호출한다고 가정하자.
# 자바 SE 7에서 다음 명령문은 컴파일되지 않는다.
processStringList(Collections.emptyList());
// List<Object> cannot be converted to List<String> 오류 발생
컴파일러는 타입 파라미터 T에 대한 값이 필요하므로 Object 값으로 시작한다. 따라서 Collections.emptryList를 호출하면 processStringList 메서드와 호환되지 않는 List<Object> 타입의 값이 반환된다.
따라서 자바 SE 7에서는 다음과 같이 타입 아규먼트의 값을 지정해야 한다.
processStringList(Collections.<String>emptyList());
자바 SE 8에서 더 이상 타입 감시가 필요하지 않다.
타겟 타입이 무엇인지에 대한 개념이 확장되어 processStringList 메서드에 대한 아규먼트와 같은 메서드 아규먼트를 포함한다.
이 경우 processStringList에는 List<String> 타입의 아규먼트가 필요하다.
Collections.emptyList 메서드는 List<T> 값을 반환하므로 List<String>의 타겟 타입을 사용하여 컴파일러는 타입 아규먼트 T의 값이 String인 것으로 유추한다.
따라서 자바 SE 8에서는 다음 코드가 컴파일 된다.
processStringList(Collections.emptyList());
import java.util.Collections;
import java.util.List;
public class TargetTypeExample {
public static void main(String[] args) {
// Java SE 7 and 8: Using target type inference
List<String> listOne = Collections.emptyList();
System.out.println("listOne: " + listOne);
// Using type witness in Java SE 7 and 8
List<String> listTwo = Collections.<String>emptyList();
System.out.println("listTwo: " + listTwo);
// Java SE 7: Type witness is required
processStringList(Collections.<String>emptyList()); // This works in both Java SE 7 and 8
// Java SE 8: Target type inference allows omitting type witness
processStringList(Collections.emptyList()); // This works in Java SE 8
}
static void processStringList(List<String> stringList) {
// Process stringList
System.out.println("Processing stringList: " + stringList);
}
}
자바에서 타겟 타입은 주로 제너릭과 람다 표현식의 문맥에서 사용되는 개념이다.
이 개념은 컴파일러가 표현식의 타입을 결정하는데 사용하는 정보를 나타낸다
1) 제너릭과 타입 추론
자바 SE 7 이전까지는 제너릭을 사용할 때, 생성자에 대한 타입 파라미터를 명시적으로 선언해야 한다.
예를 들어, List<String> 타입의 객체를 생성할 때는 new ArrayList<String>() 처럼 선언했지만 자바 SE 7 이상에서는 다이아몬드 연산자를 도입하여, 컴파일러가 문맥을 바탕으로 적절한 타입을 추론할 수 있게 되었다.
즉 new ArrayList<>()와 같이 작성하면 컴파일러는 대입 연산자 왼쪽에 명시된 List<String>을 바탕으로 오른쪽의 ArrayList의 타입 파라미터를 String으로 추론한다. 여기서 List<String>이 타겟 타입이 된다.
2) 람다 표현식
자바 SE 8에서 람다 표현식이 도입되면서 타겟 타입의 개념이 더욱 중요해졌다.
람다 표현식은 그 자체로는 타입이 명확하지 않고, 그것을 할당하는 컨텍스트(변수 타입, 메소드 파라미터)를 통해 타입이 결정된다.
예를 들어 Comparator<String> comp = (s1, s2) -> s1.compareTo(s2); 에서 람다 표현식 (s1, s2) -> s1.compareTo(s2)는
Comparator<String> 이라는 타겟 타입에 의해 결정된다.
컴파일러는 이 타입 정보를 사용하여 람다 표현식의 파라마티어 타입, 리턴 타입, 그리고 적용 가능한 인터페이스를 추론한다.
요약하자면 타겟 타입은 컴파일러가 표현식의 유형을 추론하는데 필요한 문맥적 정보를 제공한다.
제너릭에서는 다이아몬드 연산자를 사용하여 타입을 자동으로 추론할 수 있게 했고, 람다 표현식에서는 할당되는 컨텍스트를 통해 람다의 시그니처를 결정한다.
이러한 매커니즘은 코드의 간결성과 가독성을 향상시키는데 크게 기여한다.
'자바 튜토리얼' 카테고리의 다른 글
제너릭 (Generics) [4] (0) | 2024.07.18 |
---|---|
제너릭 (Generics) [3] (0) | 2024.07.17 |
제너릭 (Generics) [1] (0) | 2024.07.16 |
인터페이스와 상속 (Interface and Inheritance) [2] (0) | 2024.07.12 |
리팩토링 (Refactoring) (0) | 2024.07.11 |