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 자체가 타입으로써 사용되지는 않습니다.

 

'JAVA > Generic' 카테고리의 다른 글

7. Reification  (0) 2020.03.10
6. Generic 메소드  (0) 2020.03.10
4. Wildcard with super(하위 경계)  (0) 2020.03.06
3. Wildcard with extends(상위 경계)  (2) 2020.03.06
2. Subtyping, Substitution 원리  (0) 2020.03.05
public class Collections {
    ....
    public static <T> void copy(List<? super T> dest, List<? extends T> src){}
    ....
}

 

Collections 클래스는 List와 관련된 여러 유용한 기능을 제공합니다. 그 중 copy API를 통해서 이번절 내용을 살펴보겠습니다. 

copy는 src 변수가 가르키는 List의 데이터 값을 dest 변수가 가르키는 List에 저장하는 기능을 제공합니다.

 

여기서 우리는 지금까지 보지 못한 구문을 또 발견하게됩니다.

 

  1. ? super T
  2. <T>

 

2번째 내용은 다음 포스팅에서 다루기로하며, 이번 절에서는 ? super T 구문에 대해서 살펴보겠습니다. 

 

? super T는 무엇을 의미할까요?

이는 T 타입 뿐만 아니라 T 타입의 모든 Supertype까지 허용하겠다는 의미입니다.

즉 T 타입이 Integer라면 List<Integer> 외에 List<Number> 및 List<Object> 형태 할당을 허용하겠다는 의미입니다.

이러한 특성을 Contravariant 라고 합니다.

 

또한, Parameter Type을 T 타입 이상으로 제한한다고해서 하위 경계(Lower bound)라고 말합니다.

 

예제를 통해서 개념을 살펴보겠습니다.

 

public class Main {
    public static void main(String[] args)  {
        List<Object> objs = Arrays.asList(2, 3.14, "Penguin");
        List<Integer> ints = Arrays.asList(1, 2);
        Collections.copy(objs, ints);
        System.out.println(objs);
    }
}

 

위 코드는 Object List에 Integer List 값을 추가하는 코드입니다.

여기에서 Collections.copy 메소드는 objs 객체, ints 객체에도 속하지 않습니다. 따라서 메소드 호출 시에 어떠한 타입으로 호출될지 컴파일러가 추론하거나 개발자가 명시적로 호출시 타입을 지정해야합니다.

 

이때 지정될 수 있는 타입의 종류는 Integer, Number(Integer의 Supertype 이므로), Object입니다.

 

  1. Collections.copy(objs, ints);
  2. Collections.<Integer>copy(objs, ints);
  3. Collections.<Number>copy(objs,ints);
  4. Collections.<Object>copy(objs,ints);

 

public static <T> void copy(List<T> dest, List<? extends T> src){}

 

이러한 상황에서 만약 copy 메소드의 dst 변수 타입이 하위 경계가 아닌 T 타입으로 설계되어 있었다면 어떠한 타입으로 추론될까요?

 

public static <Object> void copy(List<Object> dest, List<? extends Object> src){}

 

위 코드를 만족시키기 위해서는 추론되는 타입이 Object 타입만 허용 가능합니다. 그 이유는 dest에 할당되는 Parameterized Type이 List<Object>이기 때문입니다.

 

따라서 위의 4가지 경우 중 1번과 4번 메소드 호출만 성공하고, 나머지 2개의 타입은 컴파일 오류가 발생합니다.

 

public static <T> void copy(List<? super T> dest, List<? extends T> src){}

 

이번에는 원래의 copy 메소드처럼 상위 경계와 하위 경계를 같이 쓰게되는 경우에는 어떻게 될까요? 결과는 위 4가지 어떠한 타입으로 추론되더라도 문제가 없습니다.

 

타입을 직접 명시하는 경우를 기준으로 하나하나씩 살펴볼까요?

 

 

1.  Collections.<Integer>copy()

public static <Integer> void copy(List<? super Integer> dest, List<? extends Integer> src){}

 

Object는 Integer의 SuperType이고, Integer는 자기 자신에 대한 Subtype이므로 위와 같이 추론될 경우 할당 가능합니다.


2. Collections.<Number>copy()

public static <Number> void copy(List<? super Number> dest, List<? extends Number> src){}

 Object는 Number Class의 SuperType이고, Integer <: Number 이므로 할당 가능합니다.


3. Collections.<Object>copy()

public static <Object> void copy(List<? super Object> dest, List<? extends Object> src){}

Object는 모든 Class의 SuperType이므로 할당 가능합니다.

 

 

위 사실들을 기반으로 사용될 수 있는 타입 파라미터 종류를 조합해보면 다음과 같은 결과를 얻을 수 있습니다.

 

1. copy(List<T> dst, List<T> src) 
   - 동일 타입만이 가능하므로, 위 예제에서는 해당 메소드를 사용할 수 없습니다.


2. copy(List<T> dst, List<? extends T> src) 
  - dst가 Object 타입이므로 Object 타입 추론될 경우에만 허용 가능합니다. 

3. copy(List<? super T> dst, List<T> src) 
  - src가 Integer 이므로 Integer 타입 추론될 경우에만 허용 가능합니다. 

4. copy(List<? suepr T> dst, List<? extends T> src) 
  - Object, Number, Integer 타입 어떤 것이 오든지 허용 가능합니다.

 

 

이를 통해 알 수 있는 사실은 적절한 경계를 사용함으로서 메소드의 타입을 추론하는데 있어서 한정된 타입만 사용되지 않도록하여 유연한 API 설계를 가능하도록 지원합니다.

 

그렇다면 하위 경계는 언제 주로 활용될까요?

전달받은 구조 내부로 T나 T의 Subtype으로 값을 입력하는 경우 주로 사용됩니다.

 

public class Main {
    public static void main(String[] args)  {
        List<Integer> ints = new ArrayList<>();
        ints.add(1);
        ints.add(3);
        List<? super Integer> ints2 = ints;
        ints.add(4);
        
        for(Integer i : ints2){   //Compile 에러 발생
           System.out.println(i);
        }
    }
}

 

위 예제와 같이 ints2 객체에서는 Integer Subtype 값 입력이 가능합니다.

반면, 해당 구조체로부터 값을 가져오려는 경우에는 컴파일 에러가 발생합니다.

그 이유는, 컴파일러 입장에서 해당 객체는 Integer의 상위 타입은 모두 올 수 있는데, 그것을 Integer 타입 변수로 할당 받는 것은 적합하지 않기 때문입니다.

 

하지만 하위 경계에도 예외가 존재합니다. 즉 하위 경계는 주로 값 입력을 위해 사용되나, 특정 타입에 경우에는 값을 가져올 수 있습니다. 그것은 바로 Object 타입입니다.

 

public class Main {
    public static void main(String[] args)  {
        List<Integer> ints = new ArrayList<>();
        ints.add(1);
        ints.add(3);
        List<? super Integer> ints2 = ints;
        ints.add(4);
        
        for(Object i : ints2){  
           System.out.println(i);
        }
    }
}

 

그 이유는 Object는 모든 클래스의 부모 클래스이기 때문입니다.

아래 예시를 통해서 자세히 알아보겠습니다.

 

List<? super Integer> 변수 사용시, 어떠한 타입이 올지 모르므로 해당 타입을 임의의 타입 변수 X라고 가정해봅시다.

이때 Integer <: X이며, X <: Object 입니다.

 

public interface List<X> extends Collection<X>{
    ...
    X get(int index);
    void add(X element);
    ...
}

 

따라서 X의 Supertype인 Object의 반환은 허용됩니다.


Get and Put Principle

 

지금까지 상위, 하위 경계를 학습하였습니다.

이전의 예시들에서 확인하였듯이 Wildcard는 사용할 수 있을 때 가능하면 사용하는 것이 좋은 습관입니다.

그 이유는 WIldcard 사용을 통해서 해당 API 호출하는 입장에서 전달하는 파라미터 내용이 내부적으로 어떻게 사용될지 유추가 가능할 뿐더러 유연한 타입 제공이 가능하기 때문입니다.

 

그렇다면 언제, 어떤 Wildcard를 사용하는 것이 좋을까요?

이를 결정하기 위한 간단한 원칙이 있습니다.(Get and Put Principle)

 

  • 하위 경계(? extends T) : 구조 바깥으로 T나 T의 Supertype으로 값을 읽어야 하는 경우 사용하자
  • 상위 경계(? super T) : 구조 내부로 T나 T의 Subtype으로 값을 입력해야하는 경우 사용하자

 

이전에 살펴본 copy 메소드를 통해 해당 원칙이 어떻게 적용되었는지 확인해봅시다.

 

public static <T> void copy(List<? super T> dst, List<? extends T> src){}

 

Get and Put 원칙을 인지한 상태에서 위와 같은 API를 보면 다음과 같은 생각을 할 수 있습니다.

 

'src 변수에 전달하는 List 객체에서는 내부에서 값을 추출하기 위한 용도로 사용되며, dst 변수에 전달하는 객체에서는 값을 입력하는 용도로 사용되겠구나'

 

이번에는 Number 타입을 지닌 Collection의 합을 구하는 메소드를 직접 디자인한다면 어떻게 타입을 설계하는것이 좋을지 살펴보도록 하겠습니다.

 

public static double sum(Collection<Number> cols){
    double s = 0.0;
    for(Number col : cols)
    	s = s + col.doubleValue();
    return s;
}    

 

먼저 위와 같이 Number 타입 지정을 고려해볼 수 있습니다. 하지만 이는 이전에 줄곧 얘기한 List<Number> 만 허용 가능한 메소드이기에 사용의 제약이 있습니다. 또한 Get and Put 원리에 따르면 해당 Collection에 별도 값 입력을 하지 않기 때문에 상위 경계(? extends Number)를 사용하는 것이 적합해보입니다.

 

이번에는 n을 입력하면 0부터 n-1까지 값을 채워주는 함수를 만들어 보겠습니다.

 

public static void assign(Collection<? super Integer> ints, int n){
    for(int i = 0 ; i < n; i++) ints.add(i);
}    

 

위 경우에는 해당 Collection 안에 값을 채워넣는 작업이 수행되므로 하위 경계를 입력하는 것이 적합해보입니다.

'JAVA > Generic' 카테고리의 다른 글

6. Generic 메소드  (0) 2020.03.10
5. Wildcard  (0) 2020.03.08
3. Wildcard with extends(상위 경계)  (2) 2020.03.06
2. Subtyping, Substitution 원리  (0) 2020.03.05
1. Generic 기초  (3) 2020.03.04

+ Recent posts