JAVA/Generic

5. Wildcard

cla9 2020. 3. 8. 18:35

Unbounded Wildcard

 

Collections 클래스에는 reverse API가 있습니다. 해당 API는 주어진 Collection내 원소들을 역순으로 뒤집어주는 기능을 수행합니다.

 

public class Collections {
    ....
    public static void reverse(List<?> list){}
    ....
}

 

위 파라미터 변수를 살펴보면 wildcard가 사용되었지만 그 어떠한 경계도 지정되지 않은 형태로 사용되었습니다.

이는 말 그대로 타입 변수가 어떠한 것이든 상관이 없음을 의미합니다.

 

자세히 보면 List<Object> 와 의미가 비슷하다고 생각할 수 있습니다. 

하지만 List<?>와 List<Object>는 다릅니다.

 

public class Main{
   public void static main(int argc, String[] argv){
       List<Object> ints = new ArrayList<>();
       ints.add(1);
       ints.add(2);
       List<?> objs;
   }
}

 

List<Object>에는 Object 의 Subtype이 할당될 수 있지만, List<?>에는 오직 null 만이 할당될 수 있습니다. null만 할당이 가능하다는 점에서 List<? extends Object>와 같습니다.

 

Unbounded Wildcard 의미는 전달되는 변수의 타입이 무엇인지 정확히는 모르나 타입은 존재하며, 관심대상은 파라미터 타입이 아닌 객체가 제공하는 기능입니다.

 

즉 위 예시의 경우에는 관심대상은 List 인터페이스가 제공하는 기능이지 그 안에서 제공되는 타입들이 제공되는 기능은 관심대상은 아닙니다.

 

 

Unbounded Wildcard 내용을 정리하면 다음과 같습니다.

  • Object 클래스에 의하여 제공되는 기본적인 기능을 사용할 경우
  • 타입 파라미터에 의존적이지 않은 Generic Class에서 제공되는 기능을 사용하는 경우
  • 매개 변수로 전달되는 실제 타입은 관심 대상이 아니다.

Wildcard Capture

 

Get and Put 원리를 적용하여 reverse API를 직접 구현해봅시다. 메소드 수행 동작상에서 List의 원소가 제공하는 기능은 관심대상이 아니기에 다음과 같이 Spec을 정의할 수 있습니다.

 

public static void reverse(List<?> list)

 

메소드를 구현하면 대략 다음과 같습니다.

 

public static void reverse(List<?> list){
    List<Object> tmp = new ArrayList<>(list);
    for(int i =0;i < tmp.size();i++){
        list.set(i, tmp.get(i));
    }
}
Error:(12, 32) java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?

 

위 메소드에서 tmp 리스트의 타입 인자가 Object인 이유는 전달받는 타입 정보를 모르기 때문에 모든 타입을 수용할 수 있도록 Object로 지정하였습니다.

 

하지만 메소드를 수행하면 에러가 발생하는 것을 볼 수 있습니다.

무엇이 문제였을까요?

 

public interface List<?> extends Collection<?>{
    ...
    ? get(int index);
    ? set(int index, ? element)
    ...
}

 

상위 경계 포스팅에서 다루었던 내용을 잠시 복습해보겠습니다.

 

Wildcard로 타입 인자가 지정되었을 때 타입에 대한 정보를 모르기때문에, 위 인터페이스에서 set 메소드를 호출할때마다 이를 지칭하기 위해 capture# of ?와 같은 형태로 컴파일러가 임의의 타입 변수를 할당합니다. 이를 Wildcard Capture라고 합니다.

 

문제는 알지 못하는 임의의 타입 T를 가르키는 capture# of ? 변수는 컴파일러가 내부적으로만 사용하고 타입 제약조건을 공식화하는데 사용하지 않는다는 점입니다. 

 

따라서 Object 타입의 값을 알 수 없는 List에 집어넣으려고 하기 때문에 에러가 발생합니다.

 

public static void reverse(List<T> list){
    List<T> tmp = new ArrayList<>(list);
    for(int i =0;i < tmp.size();i++){
        list.set(i, tmp.get(i));
    }
}

 

가장 심플한 방법은 reverse API를 위와 같이 타입 파라미터를 명시하는 방법입니다.

하지만, 관심대상이 아닌 T 타입을 컴파일러 제약때문에 API에 넣는다는 것은 그다지 좋은 디자인이 아닙니다.

따라서 API 디자인 규칙을 잘 준수하면서도 기능 동작을 구현하기위해 일종의 컴파일러 트릭을 사용합니다.

 

 

위 코드는 Helper 메소드를 사용하여 문제를 해결하였습니다. 즉 reverse 메소드를 호출할 때는 Wildcard Capture 정보를 타입 제약 조건으로 사용되지 않았습니다. 하지만 내부적으로 reverseHelper 메소드를 호출할때, 파라미터 타입에 T 타입을 명시하였기 때문에, capture# of ?와 같은 정보를 타입으로써 지정할 수 있습니다. 

 

이후 동일 타입내에서 값을 추출하고 넣음을 보장할 수 있기때문에 값 할당이 허용됩니다.


Wildcard 제한

 

public class Main {
    public static void main(String[] args)  {
        List<?> list = new ArrayList<?>(); //Compile 에러 발생
    }
}

 

 

Wildcard를 사용하면, 이를 기반으로 인스턴스 생성이 불가합니다.

만약, 타입 자동 추론을 이용하여 위 그림과 같이 list를 생성 할지라도, add 수행시 알 수 없는 타입이기 때문에 null외에 값 허용이 안됩니다.

 

이러한 제한을 둔 배경에는 Wildcard는 구체적인 타입 파라미터가 지정된 객체를 가르키는 것이기 때문에, 타입을 알 수 없는 Wildcard가 지정된 인스턴스 생성은 바람직하지 않기 때문입니다.

 

하지만 Wildcard가 쓰여있다고 해서 모든 객체가 생성 불가능한 것은 아닙니다.

Nested 형태로 Wildcard가 명시된 구조에서는 생성 가능합니다.

 

public class Main {
    public static void main(String[] args)  {
        List<List<?>> lists = new ArrayList<List<?>>();
        List<Integer> ints = Arrays.asList(1, 2, 3);
        List<Double> dbls = Arrays.asList(1.0, 2.0);
        lists.add(ints);
        lists.add(dbls);
    }
}

 

다만, 위에서도 볼 수 있듯이 Nested 형태 Wildcard가 포함된 인스턴스 또한 다른 List의 타입 파라미터를 가르키는 용도로 사용될 뿐 Wildcard 자체가 타입으로써 사용되지는 않습니다.