본문 바로가기
학습/Java

Generics의 Wildcards(와일드카드)

코동이 2024. 3. 7.

*개요

자바 문법 Generics의 Wildcards 내용을 정리합니다.

 

Wildcards란?


 지네릭 코드에서 물음표 (?) 이며, 다른 말로 알려지지 않은 타입(unknown type) 입니다. 매개변수, 필드, 로컬 변수, 리턴타입 등에 사용할 수 있습니다. (리턴타입은 구체적인 타입 선언이 바람직합니다.) 단, 와일드 카드는 지네릭 메서드 호출, 지네릭 클래스 생성, 슈퍼타입 생성에서 타입 인자(type argument)로 사용하지 않습니다. 와일드카드의 장점은 하나의 참조변수에 다양한 매개변수화된 타입을 가지는 지네릭 객체를 담는 다형성 구현입니다. 

 

 와일드카드 매개변수화된 타입은 new 인스턴스 초기화에 사용할 수 없습니다. (cannot be instantiated directly 에러 발생) 와일드카드 매개변수화된 타입은 마치 인터페이스와 비슷합니다. 참조변수가 선언될 수 있지만, 와일드카드 매개변수화된 타입의 어떠한 객체도 생성될 수 없습니다. 참조 변수는 와일드카드 매개변수화된 유형 자신 혹은 하위 타입의 객체를 참조할 수 있습니다.

 

Collection<?> coll = new ArrayList<String>();
List<? extends Number> list = new ArrayList<Long>();
Comparator<? super String> cmp = new RuleBasedCollator("< a< b< c< d");
Pair<String,?> pair = new Pair<String,String>();

List<? extends Number> list = new ArrayList<String>();  // 컴파일 에러
List<? extends Number> list = new ArrayList<? extends Number>();  // 컴파일 에러

 

type argument(타입 인자)
Box<Integer> 에서 Integer 를 뜻함, 정의된 함수, 클래스 등을 실제 호출할 때 전달되는 타입

type parameter(타입 매개변수) 
class Box<T> 에서 T를 뜻함, 함수 및 정의에서 사용되는 타입

 

 

컬렉션 요소를 출력하는 메서드에 와일드카드를 사용하는 경우 이점을 확인해보겠습니다. 첫번째는 JDK 1.5 이전 방식이고 두번째는 사용성이 낮은 방법입니다.

 

// JDK 1.5 이전 방식
void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

//오류는 없으나 사용성이 낮은 메서드
void printCollection(Collection<Object> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

 

 첫번째 메서드는 JDK 1.5 버전 이전에 사용하던 스타일입니다. 두번째 메서드는 지네릭 타입을 추가했는데 Collection<Object> 에서 Object 타입만 사용이 가능하므로 첫번째 메서드보다 오히려 사용성이 떨어집니다. Collection<Integer>, Collection<Double> 등은 사용할 수 없습니다.

 

이를 와일드카드로 개선한 코드를 확인해보겠습니다.

 

void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}

 

 Collection<?> 이므로 Collection<String>, Collection<Object>, Collection<Integer> 등 어떠한 타입도 사용이 가능합니다. Collection<?>는 모든 컬렉션 지네릭 타입의 조상타입입니다. (이를 비한정적 와일드카드라고 합니다.) for문에서 c를 Object 타입으로 사용할 수 있습니다. 컬렉션의 어떠한 타입이라도 Object를 포함하고 있기 때문에 항상 타입 안전합니다.  

 

 하지만, Object 타입으로 꺼내서 사용할 수 있어도 컬렉션에 추가는 할 수 없습니다.

 

Collection<?> c = new ArrayList<String>();

//Required : capture of ?, Provided : Object
//java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?
c.add(new Object()); // Compile time error

//add 정의
boolean add(E e) {
...

 

 컴파일 타임에  Collection<?> c 타입을 알 수 없어 객체에 추가할 수 없으므로  c.add(new Object()) 컴파일 에러가 발생합니다. 위 코드에서 add(E)에 매개변수로 capture of ? 타입이 필요하지만, Object 타입이 대입되었습니다. 타입 매개변수가 Collection<?>처럼  (?) 인 경우, unknown type(알려지지 않는 타입)입니다. unknwon type은 컴파일 타임에 실제 타입을 알 수 없습니다.  add(E)는 '매개변수는 unknown type의 자손 타입이며 E 타입을 취급해야 합니다'. 하지만 add(E)에서 사용하는 E 타입이 런타임에 unknown type의 자손임을 보장할 수 없습니다. (타입 소거 때문에 런타임에 최종적으로 Collection<?> Collection로 변합니다) 단, 모든 타입에서 공통으로 사용하는 null은 add(E) 가능합니다. 와일드카드는 컴파일 타임에 너무 추상적이므로 가능한 작업이 한정적입니다.

 

 만약, Collection<?> 대신 List<?> 였다면, 경우에 따라 get() 메서드를 호출해 리턴 값을 사용할 수 있습니다. 결과 타입은 unknown type이지만 항상 Object 타입을 포함하기 때문입니다. 그러므로 get() 결과 값을 Object 타입 값에 할당할 수 있고 Object로 기대되는 매개변수에 사용할 수 있습니다.

 

 

 

Unbounded Wildcards란?


(?) 만 사용하는 와일드카드가 비한정적 와일드카드(unbounded wildcards)입니다. 

 

비한정적 와일드카드를 사용하면 좋은 경우는 아래 2가지 경우입니다. 

 

1. Object 클래스에서 제공되는 기능을 사용하여 구현될 수 있는 메서드를 만들 때


먼저 List 원소를 출력하는 일반적인 메서드를 보겠습니다.

public static void printList(List<Object> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

 

 매개변수로 전달된 list의 원소를 순회하면서 출력합니다. 지네릭 타입이 Object이므로, Object 타입의 list만 출력이 가능합니다. 이럴 때 비한정적 와일드카드를 사용할 수 있습니다.

 

public static void printList(List<?> list) {
    for (Object elem : list)
        System.out.println(elem + " ");
    System.out.println();
}

 

위의 메서드와 다른 점은 매개변수에 List<Object> 대신 List<?> 를 사용했습니다. 비한정적 와일드카드 덕분에 List<Object>, List<Integer>, List<Double> 등 다양한 타입을 가지는 리스트의 원소를 출력 할 수 있습니다. Object의 toString() 메서드를 활용한 예제입니다. 즉 다형성을 구현할 수 있습니다. 하지만 비한정적 와일드카드는 컴파일 타임에 정확한 타입을 알 수 없기 때문에 Object 클래스가 제공하는 기능만 사용 가능합니다.

 


2. 지네릭 클래스에서 타입 인자에 의존하지 않는 메서드를 사용할 때


 

 공식문서 ArrayList.java 의 코드 일부를 확인해보겠습니다.

//ArrayList.java

public class ArrayList<E> extends AbstractList<E>
		Implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    ...
    public void size() {
        return this size;
    }

    public void clear() {
        modCount++;

        for (int i = 0; i< size; i++)
            elementData[i] = null;

        size = 0;
    }
    ...
 }

 

 예를 들어,ArrayList.java가 List.sizeList.clear 2개의 메서드만 가지고 있다면 ArrayList<?>로 사용해도 무방합니다. Class<T>의 대부분 메서드들은 T에 의존하지 않기 때문입니다. 

 

 

 공식문서 Collection.java의  코드 일부를 확인해보겠습니다.

//비한정적 와일드카드 사용
interface Collection<E> {
    public boolean containsAll(Collection<?> c);
    public boolean addAll(Collection<? extends E> c);
}

//지네릭 타입 사용
interface Collection<E> {
    public <T> boolean containsAll(Collection<T> c);
    public <T extends E> boolean addAll(Collection<T> c);
    // Hey, type variables can have bounds too!
}

 

리턴 타입이 boolean이고 각 메서드의 매개변수가 클래스 지네릭 타입과 상관이 없으므로 비한정적 와일드카드를 쓰는 것이 바람직합니다.

 

비한정적 와일드카드와 지네릭 타입을 혼용 사용할 수도 있습니다.

//비한정적 와일드카드 + 지네릭 타입 사용
class Collections {
    public static <T> void copy(List<T> dest, List<? extends T> src) {
    ...
}

//지네릭 타입만 사용
class Collections {
    public static <T, S extends T> void copy(List<T> dest, List<S> src) {
    ...
}

 

T는 첫번째 매개변수 dest와 두번째 매개변수 src에 모두 사용됩니다. 하지만 src의 매개변수화된 타입인 S는 한번만 사용되며 타입 인자와 관련이 없으므로 가능하면 명시적인 타입 매개변수보다 비한정적 와일드카드를 사용하는 것이 훨씬 가독성이 좋습니다.

 

 

와일드카드는 메서드 이외에도 필드, 지역변수, 배열에서도 사용 가능합니다.

static List<List<? extends Shape>> history 
	= new ArrayList<List<? extends Shape>>();

public void drawAll(List<? extends Shape> shapes) {
    history.addAll(shapes);
    for (Shape s : shaeps) {
    	s.draw(this);
    }
}

 

 

 공식 문서를 여러번 읽으면서 느낀 것은 다음과 같습니다. 비한정적 와일드카드를 고려하라, 그러나 해결하지 못하는 경우에 지네릭 타입(K,V, T 등등)을 사용하라. 비한정적 와일드카드는 컴파일 타임에 정확한 타입을 알 수 없다는 한계가 있어 보통 interface에서 정의할 때 사용하며 실제 로직 구현에는 Object의 기능만 사용합니다.

 

 

비한정적 와일드카드를 사용하기 좋은 예시를 확인해보겠습니다.

//잘못된 예 - 모르는 타입의 원소도 받는 로 타입을 사용했다. 
static int numElementsInCommon(Set s1, Set s2) {
    in result = 0;
    for (Object o1 : s1)
    	if (s2.contains(o1))
        	result++;
    
    return result;
}

//비한정적 와일드카드 타입을 사용하라 - 타입 안전하며 유연하다.
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

 

 비한정적 와일드카드 타입인 Set<?>은 타입 안전하지만, 로 타입 Set은 안전하지 않습니다. 로 타입에는 아무 원소나 넣을 수 있으므로 훼손하기 쉽지만 Collection<?>에는 (null 외에는) 어떤 원소도 넣을 수 없게 하였고 꺼낼 수 있는 객체의 타입도 전혀 알 수 없습니다. 따라서 비한정적 와일드카드를 사용하면 Object 클래스에서 제공하는 기능(함수)을 사용한다는 의미이며, 한정적 와일드카드를 사용하면 한정되는 클래스의 기능(함수)을 사용한다는 의미입니다. 

 

 

bounded Wildcards란?


  • Upper Bounded Wildcards (상위 제한 와일드카드)

 Upper Bounded Wildcards(상위 제한 와일드카드)는 변수 제한을 완화하기 위해 사용합니다. extends를 사용하여 unknown type을 구체적인 타입이나 '하위 타입'으로 제한합니다. 예를 들어, List<Integer> List<Double>, List<Number> 가 작동하는 메서드를 만들 때 List<? extends Number>를 사용합니다.

 

public static void process(List<? extends Foo> list) { /* ... */ }

public static void process(List<? extends Foo> list) {
    for (Foo elem : list) {
        // ...
    }
}

 

 List<? extends Foo> 덕분에, Foo 타입 혹은 Foo의 자손타입 모두를 사용할 수 있습니다.

 

 

public static double sumOfList(List<? extends Number> list) {
    double s = 0.0;
    for (Number n : list)
        s += n.doubleValue();
    return s;
}

List<Integer> li = Arrays.asList(1, 2, 3);
System.out.println("sum = " + sumOfList(li));

List<Double> ld = Arrays.asList(1.2, 2.3, 3.5);
System.out.println("sum = " + sumOfList(ld));

 

 List<? exnteds Number> 덕분에 Integer 타입 덧셈 계산도 가능하며, Double 타입 덧셈 계산도 가능합니다.

 

 

  • Lower Bounded Wildcards (하위 제한 와일드카드)

 Lower Bounded Wildcards(하위 제한 와일드카드)는 변수 제한을 완화하기 위해 사용합니다.  super를 사용하여 unkown type을 구체적인 타입이나 '상위 타입'으로 제한합니다. 예를 들어,Integer 객체 혹은 부모타입 객체인  Number, Object 를 리스트에 추가하는 메서드를 만들 때 List<? super Number>를 사용합니다.

 

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

 

 List<? super Integer> 덕분에 Integer 타입 혹은 상위 타입을 list에 추가할 수 있습니다.

 

 

와일드카드 사용 가이드


In-Out 방식


  • In: 데이터 제공, getter 사용, 코드에 데이터를 제공합니다.

Upper Bounded Wildcards(상위 제한 와일드카드)로 extends 사용

 

  • Out: 데이터 보유, setter 사용, 다양한 곳에 사용하기 위해 데이터를 보유합니다.

Lower Bounded Wildcards(하위 제한 와일드카드)로 super 사용

 

 

In-Out 방식의 예시를 copy 메서드로 알아보겠습니다.

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    int srcSize = src.size();
    if (srcSize > dest.size())
    	throw new IndexOutOfBoundsException("Source does not fit in dest");
        
    if(srcSize < COPY_THRESHOLD ||
    		(src instanceof RandomAccess & dest instanceof RandomAccess)) {
		for (int i = 0; i< srcSize; i++) {
        	dest.set(i, src.get(i));
    } else {
    	ListIterator<? super T> di = dest.listIterator();
        ListIterator<? extends T> si = src.listIterator();
        for (int i = 0; i< srcSize; i++) {
        	di.next();
            di.set(si.next());
        }
}

 

src는 복사될 데이터를 제공합니다. 따라서 in-parameter 입니다. dest는 데이터를 보유합니다. 따라서 out-parameter입니다.

 

In-Out 원리에 따라 List에 원소를 추가하려면 하위 제한 와일드카드를 사용해야 합니다.

List<? extends Number> list = new ArrayList<>();
list.add(10); //컴파일 오류, 컴파일 타임에 정확한 타입을 알 수 없다.

List<? super Integer> list2 = new ArrayList<>();
list2.add(10); //성공

 

 

PECS 방식


Producer - Extends : 생산자라면 <? extends T> 를 사용하라

 

Consumer - Super : 소비자라면 <? super T>를 사용하라

 

Stack 클래스를 확인해보겠습니다.

public class Stack<E> {
	public Stack();
    public void push(E e);
    public E pop();
    public boolean isEmpty();
}

...

public void pushAll(Iterable<E> src) {
	for (E e : src)
    	push(e);
}

push void popAll(Collection<E> dst) {
	while(!isEmpty())
    	dest.add(pop());
}

 

pushAll()과 popAll() 메서드를 정의하는데 각각 push()와 pop()을 사용합니다. 

 

지네릭을 사용하여 개선하겠습니다.

//생산자이므로 <? extends E>
public void pushAll(Iterable<? extends E> src) {
	for (E e : src)
    	push(e);
}

//소비자이므로 <? super E>
public void popAll(Collection<? super E> dst) {
	while(!isEmpty())
    	dst.add(pop());
}

 

pushAll()은 push()로 E를 생산하므로 생산자, popAll()은 pop()으로 E를 소비하고 있으므로 소비자입니다. 

 

 

 

*출처

 

http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html#Can%20I%20use%20a%20wildcard%20instantiation%20like%20any%20other%20type?

https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html

https://docs.oracle.com/javase/tutorial/extra/generics/methods.html

반응형

'학습 > Java' 카테고리의 다른 글

OOD, OOP(객체 지향 개발)의 원칙 - SOLID  (1) 2024.03.15
Generics의 Type Erasure(타입 소거)  (0) 2024.03.08
Value Object 패턴  (0) 2023.07.07
Entity vs Value Object 차이점  (0) 2023.06.30
스프링 AOP (1) - 동적 프록시  (0) 2023.01.11