엔지니어로 가는 길

자바 Type Inference 본문

프로그래밍/Java

자바 Type Inference

탐p슨 2020. 9. 23. 14:55
728x90

Type Inference

자바 컴파일러가 타입을 추론하는 것을 Type Inference라고 한다. 컴파일러는 추론을 위해 method invocation과 그에 상응하는 declaration을 살펴본다.

 

추론 알고리즘은 인자의 타입을 결정하고, 가능한 경우에 결과가 할당되는 타입 또는 리턴되는 타입까지 결정한다. 추론 알고리즘은 모든 인자와 어울리는 선(공통 부모)에서 가장 구체적인 타입을 찾는다. 아래의 pick 메소드를 살펴보자.

 

 

pick 메소드의 type 매개변수는 T이고 메소드의 매개변수 a1과 a2 모두 T이다. 하지만 pick을 호출할 때 첫 번째 인자로 String을 주었고 두 번째 인자로 ArrayList를 주었다. 이런 경우에 모든 인자와 어울리는 선(공통 부모)이란 Serializable이다. String과 ArrayList모두 Serializable을 구현하고 있기 때문이다.

 

Type Inference and Generic Methods

Type inference 덕분에 generic 메소드를 사용할 때 보통의 메소드와 마찬가지로 특정 타입을 명시하지 않은 채로 호출할 수 있다. BoxDemo 예시를 보자.

 

 

addBox라는 generic 메소드를 호출할 때 다음과 같이 type witness와 함께 type parameter를 명시할 수 있다.

 

 

하지만 Java compiler가 메소드의 인자로부터 자동으로 type 매개변수가 Integer임을 추론해주기 때문에 type witness를 생략할 수 있다.

 

 

Type Inference and Instantiation of Generic Classes

Compiler가 컨텍스트로부터 type arguments를 추론할 수 있는 경우 generic 클래스의 생성자를 호출하기 위해 필요한 type arguments를 비어 있는 type parameters (<>, the diamond)로 대체할 수 있다. 아래의 예시처럼 말이다.

 

 

Generic 클래스 객체를 생성할 때 type inference의 이점을 이용하고 싶다면 the diamond를 반드시 사용해야 한다. 다음의 경우를 보자. 

 

 

위의 statement의 결과 HashMap raw type이 생성되고, compiler는 unchecked conversion warning을 발생시킨다.

 

 

Type Inference and Generic Constructors

클래스가 Generic인지 non-generic인지와 관계 없이 생성자는 generic일 수 있다(자신만의 type 매개변수를 가질 수 있다). 다음의 예시를 보자.

 

 

이때 아래와 같이 생성자를 호출한다면,

 

 

MyClass의 type 매개변수 X에 Integer가 들어가지만 생성자의 type 매개변수 T에 String이 들어간다.

 

Java SE 7 이전의 컴파일러는 generic 생성자의 실제 type 매개변수를 추론할 수 있었다. 하지만 Java SE 7 이후의 컴파일러는 the diamond(<>)를 사용하는 경우 다음과 같이 생성되는 generic 클래스의 실제 type 매개변수까지도 추론할 수 있다.

 

 

Inference 알고리즘은 타입을 추론하기 위해 오직 invocation arguments, target types, and possibly an obvious expected return type만을 이용한다. results from later in the program를 이용하지 않는다.

 

Target Types

Java compiler는 generic 메소드 invocation의 type 매개변수를 추론하기 위해서 target typing의 이점을 이용한다. 표현식의 target type이란 표현식이 나타난 위치에 의존하여(컨텍스트에 의존하여) 자바 컴파일러가 기대하는 데이터 타입이다. 다음의 경우를 보자.

 

 

위의 statement는 List<String> 객체를 기대한다(리턴 타입을 List<String> 타입의 변수 listOne으로 받고 있기 때문이다). 이 데이터 타입을 바로 target type이라 한다. emptyList 메소드가 List<T> 타입을 리턴하기 때문에 컴파일러는 type argument T가 반드시 String일것이라고 추론한다. 이는 Java 7과 8 모두에 적용된다. 물론 type witness를 사용하여 다음과 같이 T를 명시할 수 있다.

 

 

그러나 이는 이 컨텍스트에서 불필요하다(명백하기 때문이다). Type witness가 필요한 컨텍스트를 살펴보자.

 

 

위와 같은 메소드가 있을 때, empty list로 processStringList 를 호출하고 싶다고 가정해보자.

 

 

위의 statement는 Java SE 7에서 컴파일되지 않는다. Java SE 7 컴파일러는 다음과 같은 에러 메시지를 출력한다.

 

List<Object> cannot be converted to List<String>

 

왜 그럴까? 컴파일러는 type argument T를 위한 value를 필요로 한다. 하지만 아무것도 주어지지 않았으므로 Object를 value로 삼는다. 그 결과  Collections의 emptyList 메소드는 List<Object> 객체를 리턴하는데, 이는 processStringList와 호환되지 않는다(processStringList가 원하는 것은 List<String>이기 때문이다). 따라서 Java SE 7에서는 다음과 같이 type argument의 value를 명시해야만 한다.

 

 

하지만 Java SE 8부터는 위와 같이 value를 명시해줄 필요가 없다. Target type를 결정할 때 메소드의 argument도 살피도록 확장되었기 때문이다. 이 예시에서 processStringList는 List<String> 타입의 argument를 필요로 한다. Java SE 8부터의 컴파일러는 메소드의 argument를 확인할 수 있으므로 메소드에서 원하는 것이 List<String>임을 알 수 있고, 그 결과 Collections의 emptyList 메소드가 리턴하는 List<T>의 T가 String이라고 추론할 수 있다. 따라서 JAVA SE 8부터는 다음의 statement도 컴파일된다.

 

 

참고

docs.oracle.com/javase/tutorial/java/generics/genTypeInference.html

728x90
Comments