[JAVA] 바운디드 타입이 있음에도 왜 와일드 카드라는 개념이 존재하는 걸까?

Mando·2023년 4월 1일
1

JAVA

목록 보기
4/10

제네릭의 주요 개념은 바운디드 타입과 와일드 카드라고 했다.

👋 여기서 잠깐! 바운디드 타입과 와일드 카드에 대해서 잠시 상시시키는 시간을 가져보자❗️

바운디드 타입 / 와일드 카드의 개념


바운디드 타입

Example 메서드는 타입 변수에 모든 종류의 타입을 지정할 수 있다.

public class Example<T> {

}

하지만 바운디드 타입을 이용하면
Example 메서드는 타입 변수에 Fruit 클래스를 상속받는 서브 클래스만 타입만 지정할 수 있다.

public class Example<T extends Fruit> {

}

와일드 카드

Example 메서드는 타입 변수에 모든 타입을 지정할 수 있다.

<? extneds Object>와 동일한 표현
public class Example<?> {

}

Example 메서드는 타입 변수에 Fruit 클래스와 Fruit 클래스를 상속받는 서브 클래스 타입만을 지정할 수 있다.

public class Example<? extends Fruit> {

}

Example 메서드는 타입 변수에 Fruit 클래스와 Fruit 클래스가 상속받는 슈퍼 클래스 타입만을 지정할 수 있다.

public class Example<? super Fruit> {

}

비슷한 개념인 것 같은데 왜 두 개념이 존재하는거지?


각 개념에 대해서 알고나서 두 개념이 상당히 비슷하다.. 라는 생각이 들었다.

  • 바운디드 타입, 와일드 카드 둘 다 타입 변수에 오는 형식을 제한 하는 기능이기 때문

그럼에도 불구하고 두 개념이 존재하는 이유는 무엇일까❓️

공변과 불공변


제네릭과 와일드카드에 대해 이해하기 위해서는 먼저 공변과 불공변에 대해서 알아야 한다고 한다!

  • 공변(convariant) : A 가 B의 하위 타입일 때(즉 A extends B) T<A>T<B>의 하위 타입(즉 T<A> extends T<B>) 이면 T는 공변이다.
  • 불공변(invariant) : A 가 B의 하위 타입일 때(즉 A extends B) T<A>T<B>의 하위 타입이 아니면 이면 T는 불공변이다.

🤚 대표적인 공변은 배열이다❗︎

배열은 공변이므로
Integer가 Object의 하위 타입이므로 Integer[] 도 Object[]의 하위 타입이다.

public class CovariantTest {
    public static void main(String[] args) {
        arrayTest();
    }
    public static void arrayTest(){
        Integer[] integers = {1, 2, 3};
        printArray(integers);
    }

    private static void printArray(Object[] arr){
        for (Object o : arr) {
            System.out.println(o);
        }
    }
}

🤚 대표적인 불공변은 제네릭이다❗︎

제네릭은 불공변이므로
Integer가 Object의 하위 타입이지만 List<Integer>List<Object>의 하위 타입이 아니기 떄문이다.

public class CovariantTest {
    public static void main(String[] args) {
        arrayTest();
    }
    public static void genericTest(){
        List<Integer> integers = List.of(1, 2, 3);
        
        // 컴파일 에러 
        // printArray(integers);
    }

    private static void printArray(Collection<Object> arr){
        for (Object o : arr) {
            System.out.println(o);
        }
    }
}

List와 Collection의 관계?

public interface List<E> extends Collection<E> 

List<Object>Collection<Object>의 하위 타입이기 때문에
List<Object>Collection<Object>로 형변환할 수 있다(다형성)

printArray 메서드의 매개변수를 Collection<Object> arr로 하면 컴파일 에러가 발생하지만 Collection<Integer> arr로 하면 컴파일이 잘 된다.

    private static void printArray(Collection<Integer> arr){
        for (Object o : arr) {
            System.out.println(o);
        }
    }

그럼 printArray는 Collection의 타입 변수 T가 무엇이냐에 따라 메서드를 여러 개 만들어야 하는 것인가?

    private static void printArray(Collection<String> arr){
        for (Object o : arr) {
            System.out.println(o);
        }
    }

하지만!!!!! 제네릭 타입이 다른 것은 오버로딩이 아니라 메서드 중복 정의 이다.
왜냐하면 제네릭 타입은 컴파일러가 컴파일할 때만 사용하고 제거해버리기 떄문이다(소거)

이처럼 제네릭이 공변이기 때문에 발생하는 상황을 대처하기 위해서 모든 타입을 대신할 수 있는 와일드 카드<?>가 등장했다.

와일드 카드의 등장


와일드 카드는 unknown type이다.

와일드 카드는 unknown type (정해지지 않은)이기 떄문에 Collection<?>으로 선언함으로써 모든 타입에 대한 호출이 가능해졌다.

하지만! 주의 해야할 점은 any type ❌ 이 아니라 unknown type ⭕️이라는 것이다.

public class CovariantTest {
    public static void arrayTest() {
        Collection<?> c = new ArrayList<>();
        // 컴파일 에러 발생
        // c.add(new Object());
    }

    private static void printArray(Collection<?> arr) {
        for (Object o : arr) {
            System.out.println(o);
        }
    }
}

unknown type이므로 어떤 타입이든 Collection.add 메서드의 매개변수로 들어갈 수 있는 게 아닌가?
왜 컴파일 에러가 발생하는 것일까?

Collection의 add 메서드 살펴보기

Collection의 add로 값을 추가하기 위해서는 제네릭 타입은 E 또는 E의 자식을 넣어주어야 합니다.

public interface Collection<E> extends Iterable<E> {
	    boolean add(E e);
}

하지만! 와일드카드의 경우 add로 넘겨주는 파라미터가 unknown 타입의 자식이여야 하는데, unknown type이므로 어떠한 타입을 대표하는지 알 수 없어서 자식 여부를 검사할 수 없어 컴파일 에러가 발생합니다.

한정적 와일드 카드

와일드 카드가 unknwon type이여서 발생하는 문제점을 해결하고자 한정적 와일드 카드라는 개념이 나왔습니다.

  • 한정적 와일드 카드를 사용하면 특정 타입을 기준으로 상한 범위와 하한 범위를 지정함으로써 호출 범위를 확장 또는 제한할 수 있습니다.
  • 한정적 와일드 카드에는 상한 경계 와일드 카드, 하한 경계 와일드 카드가 존재합니다.

상한 경계 와일드 카드

public class WildCardTest {
    private static void printCollection(Collection<? extends Parent>c){
        for (Parent parent : c) {
            System.out.println(parent);
        }

//        for (Child child : c) {
//            System.out.println(child);
//        }

        for (GrandParent grandParent : c) {
            System.out.println(grandParent);
        }

        for (Object object : c) {
            System.out.println(object);
        }
    }

    public static void main(String[] args) {
        ArrayList<GrandParent> grandParents = new ArrayList<>();
        grandParents.add(new Parent());
        grandParents.add(new Child());
        grandParents.add(new GrandParent());

        ArrayList<Parent> parents = new ArrayList<>();
        parents.add(new Parent());
        parents.add(new Child());

        ArrayList<Child> childs = new ArrayList<>();
        childs.add(new Child());

        // 컴파일 에러 발생
        // printCollection(grandParents);
        printCollection(parents);
        printCollection(childs);
    }
}

어떠한 이유로 컴파일 에러가 발생했는지 알아보자!

// 컴파일 에러 발생
// printCollection(grandParents);

같은 경우는 printCollection의 매개변수로는 Collection의 타입 변수가 Parent의 자식 클래스만 올 수 있기 때문이다.

printCollection(Collection<? extends Parent>c)

Parent 자식 클래스 타입은 모두 Collection의 타입 변수로 올 수 있다며! 근데 왜 Child type으로 Collection을 꺼내는 것은 안 되는 거야?

printCollection의 매개변수인<? extends Parent>으로 가능한 타입은 Parent와 미지(unknown)의 모든 Parent 자식 클래스들이다. 미지의 Parent 자식 클래스라는 것은 자식이 어떤 타입인지 정확하게 알 수 없다는 것이다.

class AnotherChild extends Parent {

}

즉, 자식 타입이 Child 일 수도 있지만 AnotherChild일 수도 있다.

따라서 Child 타입으로 객체를 꺼내려고 하면 컴파일 에러가 발생한다.
하지만 적어도 Parent 타입은 확실하므로 그 부모 타입으로 꺼내는 것은 문제가 되지 않는다

// 컴파일 에러 발생
       for (Child child : c) {
            System.out.println(child);
        }

상한 경계 와일드 카드는 컬렉션에 추가(소모)도 컴파일 에러가 발생한다고?😲

컬렉션의 타입인 <? extends Parent> 으로 가능한 타입은 Parent 클래스와 Parent 클래스의 미지의 자식 클래스이다.

하위 타입을 결정할 수 없다.❌

미지의 Parent 클래스의 자식 클래스 -> 우리는 미지의 자식 클래스가 어떤 클래스인지 정확하게 모른다.
따라서 하위 타입을 결정할 수 없다.
ex) Collection의 타입 변수로 들어올 수 있는 미지의 자식 클래스가 Child 일수도, AnotherChild일 수도 있다.

상위 타입을 결정할 수 없다.❌

우선 Collection의 타입으로 Parent 클래스와 Parent 클래스의 미지의 자식 클래스만이 올 수 있다.
따라서 최상의 상위 타입은 Parent 클래스이다.
그렇기에 GrandParent 클래스를 추가할 수 없다.
또한, Collection c의 매개변수가 정확하게 어떤 타입인지 모르기 때문에 상위 타입을 결정할 수 없다.

    private static void printCollection(Collection<? extends Parent>c){
        // 컴파일 에러 발생
        // Parent 클래스의 미지의 자식 클래스만 가능 -> 따라서 하위 타입을 정확하게 결정하지 못 한다.
        c.add(new Child());
        // 컴파일 에러 발생
        c.add(new AnotherChild());

        // 컴파일 에러 발생
        // Parent 클래스 뿐만 아니라 Parent 클래스의 하위 타입도 올 수 있다. 따라서 상위 타입을 결정하지 못 한다.
        c.add(new Parent());

        // 컴파일 에러 발생
        // Collection의 타입으로 Parent, Parent 클래스의 하위 타입만 올 수 있다.
        c.add(new GrandParent());

    }

    public static void main(String[] args) {
        // Collection의 타입이 GrandParent인 경우 -> GrandParent의 자식 클래스만 가능
        ArrayList<GrandParent> grandParents = new ArrayList<>();
        grandParents.add(new GrandParent());
        grandParents.add(new Parent());
        grandParents.add(new Child());
        grandParents.add(new AnotherChild());

        // Collection의 타입이 Parent 인 경우 -> Parent의 자식 클래스만 가능
        ArrayList<Parent> parents = new ArrayList<>();
        parents.add(new Parent());
        parents.add(new Child());
        parents.add(new AnotherChild());

        // Collection의 타입이 Child 인 경우 -> Child의 자식 클래스만 가능
        ArrayList<Child> childs = new ArrayList<>();
        childs.add(new Child());
        // 컴파일 에러 발생
        childs.add(new AnotherChild());

        ArrayList<AnotherChild> anotherChilds = new ArrayList<>();
        anotherChilds.add(new AnotherChild());
        // 컴파일 에러 발생
        anotherChilds.add(new Child());

        // 컴파일 에러 발생
        printCollection(grandParents);
        printCollection(parents);
        printCollection(childs);
        printCollection(anotherChilds);
    }

이런 식으로 원소를 소모하는 경우에는 하한 경계를 지정하여 최소한 Parent 타입임을 보장하면 문제를 해결할 수 있다.

하한 경계 와일드 카드

상한 경계 와일드 카드는 컬렉션에 추가(소모)도 컴파일 에러가 발생한다고?😲

컬렉션의 타입인 <? super Parent> 으로 가능한 타입은 Parent 클래스와 Parent 클래스의 미지의 부모 클래스이다.

하위 타입을 결정할 수 있다.⭕️

Collection의 타입은 적어도 Parent 클래스이다.
따라서 Parent 클래스의 자식 클래스는 Collection에 값을 추가할 수 있다.(소모 가능)

상위 타입을 결정할 수 없다.❌

Parent 클래스와 Parent 클래스의 미지의 부모 클래스 -> 따라서 우리는 미지의 부모 클래스가 정확하게 어떤 클래스인지 모른다.
ex) Collection의 타입 변수로 들어올 수 있는 미지의 부모 클래스가 GrandParent 일수도, AnotherGrandParent일 수도 있다.

 private static void printCollection(Collection<? super Parent>c){
        // Collection의 타입으로 Parent 클래스와 Parent 클래스의 부모 클래스가 올 수 있다.
        // 따라서 Parent 클래스의 자식 클래스들은 소모가 가능하다.
        c.add(new Child());
        c.add(new AnotherChild());
        c.add(new Parent());

        // 컴파일 에러 발생
        // Parent 클래스의 미지의 부모 클래스만 가능 -> 따라서 상위 타입을 정확하게 결정하지 못 한다.
        c.add(new GrandParent());
      
    }

    public static void main(String[] args) {
        // Collection의 타입이 GrandParent인 경우 -> GrandParent의 자식 클래스만 가능
        ArrayList<GrandParent> grandParents = new ArrayList<>();
        grandParents.add(new GrandParent());
        grandParents.add(new Parent());
        grandParents.add(new Child());
        grandParents.add(new AnotherChild());
        
        ArrayList<Parent> parents = new ArrayList<>();
        parents.add(new Parent());
        parents.add(new Child());
        parents.add(new AnotherChild());
        
        ArrayList<Child> childs = new ArrayList<>();
        childs.add(new Child());
        // 컴파일 에러 발생
//         childs.add(new AnotherChild());

        ArrayList<AnotherChild> anotherChilds = new ArrayList<>();
        anotherChilds.add(new AnotherChild()); 
        // 컴파일 에러 발생
        // anotherChilds.add(new Child());
        
        printCollection(grandParents);
        printCollection(parents);
        
        // / Collection의 타입은 Parent 클래스와 Parent 클래스의 부모 클래스이다.
        // 컴파일 에러 발생
        printCollection(childs);
        // 컴파일 에러 발생
        printCollection(anotherChilds);
    }
    private static void printCollection(Collection<? extends Parent>c){
        // 컴파일 에러 발생
        // Parent 클래스의 미지의 자식 클래스만 가능 -> 따라서 하위 타입을 정확하게 결정하지 못 한다.
        c.add(new Child());
        // 컴파일 에러 발생
        c.add(new AnotherChild());

        // 컴파일 에러 발생
        // Parent 클래스 뿐만 아니라 Parent 클래스의 하위 타입도 올 수 있다. 따라서 상위 타입을 결정하지 못 한다.
        c.add(new Parent());

        // 컴파일 에러 발생
        // Collection의 타입으로 Parent, Parent 클래스의 하위 타입만 올 수 있다.
        c.add(new GrandParent());

    }

    public static void main(String[] args) {
        // Collection의 타입이 GrandParent인 경우 -> GrandParent의 자식 클래스만 가능
        ArrayList<GrandParent> grandParents = new ArrayList<>();
        grandParents.add(new GrandParent());
        grandParents.add(new Parent());
        grandParents.add(new Child());
        grandParents.add(new AnotherChild());

        // Collection의 타입이 Parent 인 경우 -> Parent의 자식 클래스만 가능
        ArrayList<Parent> parents = new ArrayList<>();
        parents.add(new Parent());
        parents.add(new Child());
        parents.add(new AnotherChild());

        // Collection의 타입이 Child 인 경우 -> Child의 자식 클래스만 가능
        ArrayList<Child> childs = new ArrayList<>();
        childs.add(new Child());
        // 컴파일 에러 발생
        childs.add(new AnotherChild());

        ArrayList<AnotherChild> anotherChilds = new ArrayList<>();
        anotherChilds.add(new AnotherChild());
        // 컴파일 에러 발생
        anotherChilds.add(new Child());

        // 컴파일 에러 발생
        printCollection(grandParents);
        printCollection(parents);
        printCollection(childs);
        printCollection(anotherChilds);
    }

Parent 부모 클래스 타입은 모두 Collection의 타입 변수로 올 수 있다며❗️ 근데 왜 Collection을 Parent, Parent 자식 타입, Parent 부모 타입으로 꺼내는 게 안 되는건데❓️❓️❓️

printCollection의 매개변수인<? super MyParent>으로 가능한 타입은 Parent와 미지(unknown)의 모든 Parent 부모 클래스들이다. 미지의 Parent 부모 클래스라는 것은 부모가 어떤 타입인지 정확하게 알 수 없다는 것이다.

    private static void printCollection(Collection<? super Parent> c) {	
    	// 컴파일 에러 발생
        for (Child child : c) {
            System.out.println(child);
        }
		
        // 컴파일 에러 발생
        for (AnotherChild anotherChild : c) {
            System.out.println(anotherChild);
        }

		// 컴파일 에러 발생
        for (Parent parent : c) {
            System.out.println(parent);
        }
        
        // 컴파일 에러 발생
        for (GrandParent grandParent : c) {
            System.out.println(grandParent);
        }
		
       	// 컴파일 정상 작동
        for (Object object : c) {
            System.out.println(object);
        }
    }

즉, 부모 타입이 GrandParent 일 수도 있지만 AnotherGrandParent일 수도 있다.
따라서 GrandParent 타입으로 객체를 꺼내려고 하면 컴파일 에러가 발생한다.

class AnotherGrandParent extends Parent {

}
// 컴파일 에러 발생
for (GrandParent grandParent : c) {
	System.out.println(grandParent);
}

Collection의 타입으로는 Parent 클래스와, Parent의 부모 클래스가 올 수 있다.
하지만 이를 Parent와 Parent의 자식 클래스 타입으로 꺼내려고 한다면 에러가 발생한다.
왜냐하면 부모 클래스를 자식 클래스로 형변환을 하려고 하면 명시적으로 해야하기 때문이다.( -> 정확한 표현을 모르겠다.)

    private static void printCollection(Collection<? super Parent> c) {	
    	// 컴파일 에러 발생
        for (Child child : c) {
            System.out.println(child);
        }
		
        // 컴파일 에러 발생
        for (AnotherChild anotherChild : c) {
            System.out.println(anotherChild);
        }

		// 컴파일 에러 발생
        for (Parent parent : c) {
            System.out.println(parent);
        }	
    }
// 업캐스팅 : 자식 클래스의 객체가 부모 클래스 타입으로 형변환 되는 것 (다형성)
parent=child;

// 다운캐스팅 : 부모 클래스 객체가 자식 클래스 타입으로 형변환 되는 것 (다운캐스팅을 할 때는 명시적으로 지정해주어야 한다.)
child=(Child)parent;
    private static void printCollection(Collection<? super Parent> c) {	
       	// 컴파일 정상 작동
        for (Object object : c) {
            System.out.println(object);
        }```java
        
    }

Object는 모든 클래스의 부모 클래스이므로 Parent 타입으로 형변환할 수 있다.(암묵적인 형변환)

PECS(Producer - Extends, Consumer - Super) 공식

언제 Super를 사용해야 하고, 언제 extends를 사용해야 하는지 헷갈릴 수 있다.
즉, 컬렉션으로부터 와일드카드 타입의 객체를 생성 및 만들면(produce) extends를, 갖고 있는 객체를 컬렉션에 사용 또는 소비(consumer)하면 super을 사용하라는 것이다.

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

void addElement(Collection<? super MyParent> c) {
    c.add(new MyParent());
}

객체를 생성 및 만들면(produce)하면 extends를 사용

Collection의 타입 변수로 Parent 또는 Parent의 미지의 자식 클래스만 가능하다.
그러면 미지의 자식 클래스가 정확하게 어떤 타입인지 모르기 때문에 Parent의 자식 클래스로 객체를 생성하면 컴파일 에러가 발생한다.

하지만, Parent의 부모 클래스로 객체를 생성하는 것은 가능하다.
왜냐하면 자식 클래스를 부보 클래스로 업캐싱하는 것은 암묵적으로 가능하기 떄문이다.

void printCollection(Collection<? extends Parent> c) {

		// 컴파일 에러 발생
        for (Child child : c) {
            System.out.println(child);
        }
		
        // 컴파일 에러 발생
        for (AnotherChild anotherChild : c) {
            System.out.println(anotherChild);
        }

        for (Parent parent : c) {
            System.out.println(parent);
        }
        for (GrandParent grandParent : c) {
            System.out.println(grandParent);
        }

        for (Object object : c) {
            System.out.println(object);
        }
}

갖고 있는 객체를 컬렉션에 사용 또는 소비(consumer)하면 super을 사용

Collection의 타입 변수로 Parent 또는 Parent의 미지의 부모 클래스만 가능하다.
따라서 Parent의 자식 클래스를 컬렉션에 사용 또는 소비(consumer)하지 못하고
또한 미지의 부모 클래스이므로 Parent의 부모 클래스 중 정확하게 어떤 클래스인지 모르기 때문에 Parent의 부모 클래스를 소비 하는 것도 컴파일 에러가 발생한다.

    private static void printCollection(Collection<? super Parent> c) {
        c.add(new Child());
        c.add(new AnotherChild());
        c.add(new Parent());

		// 아래는 컴파일 에러 발생
        c.add(new GrandParent());
        c.add(new AnotherGrandParent());
}

0개의 댓글