✅ Wildcards
일반 코드에서 와일드카드라고 하는 물음표(?)는 알 수 없는 타입을 나타냅니다. 와일드카드는 다양한 상황에서 사용할 수 있습니다. 때로는 반환 타입으로 사용됩니다. 와일드카드는 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성 또는 상위 타입에 대한 타입 아규먼트로 사용되지 않습니다.
다음 섹션에서는 상한 와일드카드(Upper Bounded Wildcards), 하한 와일드카드(Lower Bounded Wildcards) 및 와일드카드 캡처(Wildcards Capture)를 포함하여 와일드카드에 대해 자세히 설명합니다.
✅ Upper Bounded Wildcards
- 상위 타입 경계(Upper bound) → 타입의 최상단 경계를 지정한다.
Upper Bounded Wildcards를 사용하여 변수에 대한 제네릭의 제한을 완화할 수 있습니다.(=상위 타입을 제한하여 해당 타입의 하위 타입(상속 받은)까지 제네릭 타입으로 사용할 수 있습니다.) 예를 들어 List<Integer>, List<Double> 및 List<Number>에서 작동하는 메서드를 작성한다고 가정해 봅시다. 상한 와일드카드를 사용하여 이를 달성할 수 있습니다.
Upper Bounded Wildcards를 선언하려면 와일드카드 문자('?'), extends 키워드, upper bound을 차례로 사용합니다. 이 컨텍스트에서 extends은 일반적인 의미에서 "extends"(클래스에서의 상속) 또는 "implements"(인터페이스에서의 구현)을 의미하는 데 사용됩니다.
Number과 Integer, Double 및 Float와 같은 Number의 하위 타입의 List에서 작동하는 메서드를 작성하려면 List<? extends Number> 코드를 정의합니다. List<Number>라는 용어는 List<? extneds Number>보다 더 제한적입니다. 전자가 Number 타입의 List에만 일치하는 반면 후자는 Number 타입의 List 또는 해당 하위 클래스와 일치하기 때문입니다. 그래서 섹션 시작시 언급한 "Upper Bounded Wildcards”를 사용하여 변수에 대한 제네릭의 제한을 완화할 수 있습니다.
- Consider the following process method
public static void process(List<? extends Foo> list) { /* ... */ }
upper bounded wildcard, <? extends Foo>, 여기서 Foo는, Foo 및 Foo의 모든 하위 타입과 일치합니다. process 메소드는 Foo 타입으로 List 요소에 액세스할 수 있습니다.
public static void process(List<? extends Foo> list) { for (Foo elem : list) { // ... } }
for-each 절에서 elem 변수는 list의 각 요소를 반복합니다. 이제 Foo 클래스에 정의된 모든 메서드들은 elem변수를 통해 사용할 수 있습니다.
- The sumOfList method returns the sum of the numbers in a list
public static double sumOfList(List<? extends Number> list) { double s = 0.0; for (Number n : list) s += n.doubleValue(); return s; }
다음 코드는 Integer 객체 list을 사용하여 sum = 6.0을 print합니다.
List<Integer> li = Arrays.asList(1, 2, 3); System.out.println("sum = " + sumOfList(li));
Double value을 요소로 가지는 list은 동일한 sumOfList 메서드를 사용할 수 있습니다. 다음 코드는 sum = 7.0을 print합니다.
List<Double> ld = Arrays.asList(1.2, 2.3, 3.5); System.out.println("sum = " + sumOfList(ld));
- Consider the following process method
✅ Unbounded Wildcards
unbounded 와일드카드 타입은 와일드카드 문자(?)를 단독으로 사용하여 지정됩니다(예: List<?>). 이를 알 수 없는 타입의 list라고 합니다. unbounded 와일드카드가 유용한 접근 방식인 두 가지 시나리오가 있습니다.
- Object 클래스에서 제공하는 기능을 사용하는, 구현될 수 있는 메서드를 작성하는 경우.
👉 이는 타입 파라미터로 오는 와일드 카드가 “?”이므로 해당 타입이 Object임을 기대한다고 생각할 수도 있습니다.
- 코드가 타입 파라미터에 의존하지 않는 제네릭 클래스의 메서드를 사용하는 경우. 예를 들어 List.size 또는 List.clear입니다. 실제로 Class<?>는 Class<T>의 대부분의 메서드가 T에 의존하지 않기 때문에 자주 사용됩니다.
- Consider the following method, printList
public static void printList(List<Object> list) { for (Object elem : list) System.out.println(elem + " "); System.out.println(); }
printList의 목표는 모든 타입의 list을 print 하는 것이지만 해당 코드는 목표를 달성하는 데 실패했습니다. 위 코드는 Object 인스턴스의 list만(
List<Object>
) print 합니다. List<Integer>, List<String>, List<Double> 등은 List<Object>의 하위 타입이 아니기 때문에 print할 수 없습니다. 일반 printList 메소드를 작성하려면 List<?>를 사용해야 합니다.public static void printList(List<?> list) { for (Object elem: list) System.out.print(elem + " "); System.out.println(); }
구체적인 유형 A(예시에서 String, Integer로 사용됨)의 경우 List<A>는 List<?>의 하위 유형이므로 printList를 사용하여 모든 유형의 목록을 인쇄할 수 있습니다.
List<Integer> li = Arrays.asList(1, 2, 3); List<String> ls = Arrays.asList("one", "two", "three"); printList(li); printList(ls);
List<Object>와 List<?>는 동일하지 않다는 점에 유의해야 합니다. Object 또는 Object의 하위 타입을 List<Object>에 삽입할 수 있습니다. 그러나 List<?>에는 null만 삽입할 수 있습니다. Guidelines for Wildcard Use 에는 주어진 상황에서 어떤 종류의 와일드카드를 사용해야 하는지 결정하는 방법에 대한 자세한 정보가 있습니다.
- List<?>에는 null만 추가할 수 있는 이유
List<?>
는 "모든 타입"을 나타내는 와일드카드 타입입니다. 이 타입의 리스트에는 어떤 타입의 요소도 포함할 수 있습니다. 그러나List<?>
에는 null 값만 추가할 수 있는 제약이 있습니다.이는 와일드카드 타입의 한계로 인해 발생하는 제약입니다. 와일드카드(”?”) 타입은 "알 수 없는 타입"을 나타내므로, 컴파일러는 어떤 구체적인 타입이 올지 확신할 수 없습니다. 따라서, 타입 안전성을 보장하기 위해 null 값만이 유효한 값으로 간주됩니다.
예를 들어,
List<?>
에는List<Integer>
,List<String>
,List<Double>
과 같은 구체적인 타입의 리스트를 할당할 수 있습니다. 그러나 이러한List<?>
에는 해당 타입의 요소를 추가할 수 없습니다. 왜냐하면 컴파일러는 리스트의 구체적인 타입이 무엇인지 알 수 없기 때문에 타입 안전성을 보장하기 위해 요소의 타입을 체크할 수 없습니다. 따라서, null 값 외에는 다른 값을 추가할 수 없습니다.만약
List<?>
에 다른 값을 추가할 수 있다면, 컴파일러는 리스트에 저장된 요소의 타입을 알 수 없기 때문에 타입 안전성이 보장되지 않을 수 있습니다. 예를 들어,List<Integer>
타입의 리스트에 String 값을 추가하는 것은 타입 불일치로 인해 런타임 에러가 발생할 수 있습니다. 이와 같이 컴파일러는List<?>
에 대해서 어떤 타입이 지정될지 모르기 때문에 타입 안전성을 유지하기 위해List<?>
에는 null 값만이 유효한 값으로 간주하고 추가할 수 있도록 제한합니다.List<?> list = new ArrayList<>(); list.add(null); // 가능 list.add("Hello"); // 컴파일 오류 list.add(10); // 컴파일 오류 list.add(3.14); // 컴파일 오류
따라서,
List<?>
에는 null 값 외에는 다른 값을 추가할 수 없습니다. 이는 와일드카드 타입의 제약으로 인해 발생하는 것입니다.👉 와일드카드 타입
List<?>
에는 null 값만이 추가될 수 있는 이유는 타입 체크의 한계 때문입니다. 컴파일러는List<?>
가 어떤 구체적인 타입의 리스트인지 알 수 없으므로, 리스트에 요소를 추가할 때 해당 요소의 타입을 체크할 수 없습니다. 이는 컴파일러가 타입 안전성을 유지하기 위해 취하는 조치입니다.
- List<?>에는 null만 추가할 수 있는 이유
✅ Lower Bounded Wildcards
- 하위 타입 경계(Lower Bound) → 타입의 최하단 경계를 지정한다.
상한 와일드카드 섹션에서는 상한 와일드카드가 알 수 없는 타입을 특정 타입 또는 해당 타입의 하위 타입으로 제한하고 extends 키워드를 사용하여 표시됨을 보여줍니다. 비슷한 방식으로 Lower Bounded Wildcards는 알 수 없는 타입을 특정 타입 또는 해당 타입의 상위 타입으로 제한합니다.
Lower Bounded Wildcards는 와일드카드 문자('?'), super 키워드, lower bound을 차례로 사용하여 표현됩니다. <? super A>.
Integer 객체를 List에 넣는 메서드를 작성하고 싶다고 가정해 보겠습니다. 유연성을 극대화하기 위해 메서드가 List<Integer>, List<Number> 및 List<Object> → Integer 값을 보유할 수 있는 모든 요소에서 작동하기를 원합니다. Integer, Number 및 Object와 같은 Integer의 상위 타입 및 Integer list에서 작동하는 메서드를 작성하려면 List<? super Integer>을 지정하면 됩니다. List<Integer>라는 용어는 List<? super Integer> 보다 더 제한적입니다. 전자는 Integer 타입의 list에만 일치하지만 후자는 Integer의 상위 타입인 모든 타입의 list과 일치하기 때문입니다.
public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } list.add(new Object()); list.add(new Number()); list.add(new Integer()); }
👉 위 코드는 와일드 카드를 사용해서 Lower Bouned가 어디까지 제한하는지 나타내는 예시 코드입니다. 자기 자신을 포함하여, 상속 받은 클래스의 타입까지 지정할 수 있다는 것을 보여줍니다.
✅ Wildcards and Subtyping
- 와일드 카드와 타입 상속 관계
Generics, Inheritance, and Subtypes에서 설명한 것처럼 제네릭 클래스 또는 인터페이스는 단순히 타입간에 관계가 있기 때문에 타입의 상속과는 관련이 없습니다. 그러나 와일드카드를 사용하여 제너릭 클래스 또는 인터페이스 간의 타입 상속 관계를 만들 수 있습니다.
- 다음 두 개의 정규(제네릭이 아닌) 클래스가 주어집니다.
class A { /* ... */ } class B extends A { /* ... */ }
다음 코드를 작성하는 것은 합리적입니다.
B b = new B(); A a = b;
이 예제는 일반 클래스의 상속이 subtype 지정 규칙을 따른다는 것을 보여줍니다. B가 A를 확장하는 경우, 클래스 B는 클래스 A의 subtype입니다. 그러나 이 규칙은 제네릭 유형에는 적용되지 않습니다.
List<B> lb = new ArrayList<>(); List<A> la = lb; // compile-time error
- Integer가 Number의 subtype인 경우 List<Integer>와 List<Number> 사이의 관계는 어떻게 될까요?
Integer는 Number의 subtype이지만 List<Integer>는 List<Number>의 subtype이 아니며 실제로 이 두 type은 관련이 없습니다. List<Number> 및 List<Integer>의 공통 부모는 List<?>입니다. 코드가 List<Integer>의 요소를 통해 Number의 메서드에 액세스할 수 있도록 이러한 클래스 간의 관계를 만들려면 Upper Bounded Wildcard를 사용합니다.
List<? extends Integer> intList = new ArrayList<>(); List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>
Integer는 Number의 subtype이고 numList는 Number 객체 리스트이므로 이제 intList(Integer 객체 목록)와 numList 간에 관계가 존재합니다. 다음 다이어그램은 upper 및 lower bounded 와일드카드로 선언된 여러 List 클래스 간의 관계를 보여줍니다.
👉
List<? extends Integer>
는Integer
를 상속받은 타입들로 이루어진 리스트를 나타냅니다.List<? extends Number>
는Number
를 상속받은 타입들로 이루어진 리스트를 나타냅니다.List<? extends Integer>
가List<? extends Number>
의 하위 타입이 되는 이유는 다음과 같습니다.List<? extends Integer>
의 요소 타입은Integer
또는Integer
를 상속받은 타입들입니다.
Integer
는Number
의 하위 타입이므로,List<? extends Integer>
의 요소 타입은Number
의 하위 타입들입니다.
- 따라서,
List<? extends Integer>
의 요소 타입은List<? extends Number>
의 요소 타입으로 사용할 수 있습니다.
이를 통해
List<? extends Integer>
는List<? extends Number>
의 하위 타입으로 취급될 수 있습니다. 따라서List<? extends Number>
변수에List<? extends Integer>
를 할당하는 것은 가능합니다.이러한 제약은 공변성(invariance)의 원칙에 따라 이루어지는데,
List
인터페이스는 공변성을 가지지 않기 때문에List<Integer>
를List<Number>
에 할당할 수 없습니다. 하지만 와일드카드 타입을 사용하면 타입 간의 상속 관계에 따라 조금 더 유연하게 대응할 수 있습니다.
- Integer가 Number의 subtype인 경우 List<Integer>와 List<Number> 사이의 관계는 어떻게 될까요?
✅ Wildcard Capture and Helper Methods
- 와일드 카드 캡쳐와 지원되는 메소드
어떤 경우에는 컴파일러가 와일드카드 타입을 유추합니다. 예를 들어 List은 List<?>로 정의될 수 있지만 표현식을 평가할 때 컴파일러는 코드에서 특정 타입을 유추합니다. 이 시나리오를 와일드카드 캡처라고 합니다. 대부분의 경우 "capture of"라는 문구가 포함된 오류 메시지가 표시되는 경우를 제외하고는 와일드카드 캡처에 대해 걱정할 필요가 없습니다.
- WildcardError 예제는 컴파일 시 캡처 오류를 생성합니다.
import java.util.List; public class WildcardError { void foo(List<?> i) { i.set(0, i.get(0)); } }
이 예제에서 컴파일러는 List<?> i 에 대한 알 수 없는 입력 파라미터를 내부적으로 Object 타입으로 처리합니다. foo 메서드가 List.set(int, E)를 호출하면 컴파일러는 리스트에 삽입되는 객체의 타입을 확인할 수 없으며 오류가 생성됩니다. 이러한 타입의 오류가 발생하면 일반적으로 컴파일러가 여러분이 잘못된 타입을 변수에 지정하고 있다고 해석한다는 의미입니다. 이러한 이유로 제네릭은 Java 언어에 컴파일 시 타입 안전성을 강화하기 위해 추가되었습니다.
👉와일드 카드 컴파일 오류에 대한 상세 해석
두 번째 코드의 와일드카드(
?
)로 선언된List<?> i
는 컴파일러에게 "알 수 없는 타입"이라는 정보를 제공합니다. 이 경우 컴파일러는?
에 대한 실제 타입을 정확히 알 수 없으며, 따라서 내부적으로는Object
타입으로 처리합니다.이로 인해
i
매개변수에 대해서 컴파일러는 실제로 어떤 타입의 리스트인지 알 수 없게 됩니다. 즉, 모든 타입으로 올 수 있기 때문에 어떤 타입이 올 것인지 추론이 불가능합니다. 컴파일러는List<?>
를List<Object>
로 간주하지 않기 때문에i
에 대해 호출할 수 있는 메서드가 제한됩니다.따라서
i.set(0, i.get(0))
와 같은 리스트의 요소를 변경하는 작업은 컴파일러가 허용하지 않습니다. 컴파일러는?
에 대한 실제 타입이 무엇인지 알 수 없기 때문에, 리스트의 요소를 어떤 타입으로 변경해야 하는지 판단할 수 없습니다. 이로 인해 컴파일러는 해당 코드를 오류로 처리합니다.정리하자면, 두 번째 코드에서 와일드카드(
?
)를 사용한 매개변수List<?> i
는 컴파일러에게 해당 리스트의 요소 타입이 무엇인지 알려주지 않기 때문에, 컴파일러는i
를List<Object>
로 처리하지 않으며, 따라서 요소 변경과 같은 작업에 대해 안전성을 보장할 수 없어 컴파일 오류가 발생합니다.➡️ 제네릭 메서드를 정의할 때
<T>
와 와일드카드(?
)의 차이점은 다음과 같습니다.- 제네릭 메서드
<T> void foo(List<T> i)
: 이 경우, 메서드를 호출할 때 명시적으로 타입 매개변수T
를 지정해주어야 합니다. 예를 들어,foo<Integer>(list)
와 같이 호출하면T
는Integer
로 결정됩니다. 따라서 해당 메서드는 특정한 타입에 대해서만 작동하며, 호출할 때마다 타입을 명시적으로 지정해주어야 합니다. 이는 호출하는 쪽에서 제약을 받는 점입니다.
- 와일드카드(
?
)를 사용한 메서드void foo(List<?> i)
: 이 경우, 메서드를 호출할 때 타입을 명시적으로 지정해주지 않아도 됩니다.List<Integer>
,List<String>
과 같이 다양한 타입의 리스트를 인자로 전달할 수 있습니다. 와일드카드(?
)는 어떤 타입이든 가능하다는 뜻이며, 호출하는 쪽에서 유연성을 가지게 됩니다.
➡️ 따라서,
<T>
와 와일드카드(?
)는 다음과 같은 차이점을 가집니다:<T>
는 특정한 타입에 대해서만 작동하며, 호출할 때마다 타입을 명시적으로 지정해주어야 합니다.
- 와일드카드(
?
)는 모든 타입을 다룰 수 있으며, 호출할 때 타입을 명시적으로 지정할 필요가 없습니다.
결론적으로, 제네릭 메서드의 매개변수 타입에
<T>
대신 와일드카드(?
)를 사용하는 경우, 해당 메서드는 더욱 유연하게 다양한 타입의 리스트를 다룰 수 있습니다. 결국 와일드 카드를 사용해서 호출할 때 타입을 명시적으로 지정해주지 않아도 되는 이점 때문에, 컴파일러가 해당 타입이 어떤 것이 올지 알 수 없고, 이에 따라 타입 안정성 때문에 알 수 없는 정보를 가진 와일드 카드로 인해 컴파일 오류가 발생합니다.- 제네릭 메서드
➡️ WildcardError 예제는 Oracle의 JDK 7 javac으로 컴파일할 때 다음 오류를 생성합니다.
WildcardError.java:6: error: method set in interface List<E> cannot be applied to given types; i.set(0, i.get(0)); ^ required: int,CAP#1 found: int,Object reason: actual argument Object cannot be converted to CAP #1 by method invocation conversion where E is a type-variable: E extends Object declared in interface List where CAP#1 is a fresh type-variable: CAP#1 extends Object from capture of ? 1 error
이 샘플에서 코드는 안전한 작업을 수행하려고 시도하므로 컴파일러 오류를 어떻게 해결할 수 있을까요? 와일드카드를 캡쳐하는 private 도우미 메서드를 작성하여 문제를 해결할 수 있습니다. 이 경우 WildcardFixed에 표시된 것처럼 private 헬퍼 메서드인 fooHelper를 생성하여 문제를 해결할 수 있습니다.
public class WildcardFixed { void foo(List<?> i) { fooHelper(i); } // Helper method created so that the wildcard can be captured // through type inference. private <T> void fooHelper(List<T> l) { l.set(0, l.get(0)); } }
👉 아래는
foo()
메소드를 호출하여 사용하는 예시 코드입니다.List<Integer> integerList = new ArrayList<>(); integerList.add(10); integerList.add(20); WildcardFixed wildcardFixed = new WildcardFixed(); wildcardFixed.foo(integerList);
foo()
메소드는List<?>
타입의 매개변수i
를 받습니다. 이 메소드를 호출할 때,integerList
와 같은 구체적인 타입의 리스트를 전달합니다.foo()
메소드 내부에서는fooHelper(i)
를 호출합니다.fooHelper()
메소드는 실제 요소의 타입을 포함한List<T>
타입의 매개변수l
을 받습니다. 이때,T
는fooHelper()
메소드를 호출할 때 타입 추론에 의해 캡처(포착)되는 타입입니다.즉,
foo(i)
에서 전달된integerList
의 타입Integer
가fooHelper()
메소드의 타입 인자T
로 캡처되어List<Integer>
로 인식됩니다. 그래서l
은List<Integer>
타입으로 처리됩니다.fooHelper()
메소드 내부에서는 실제로 요소에 접근하고 수정하는 작업이 이루어집니다.l.set(0, l.get(0))
는 리스트의 첫 번째 요소를 가져와서 다시 첫 번째 위치에 설정하는 작업을 수행합니다.이렇게
foo()
메소드를 호출하여 사용하면,List<?>
타입을 이용하여 다양한 구체적인 타입의 리스트를 전달할 수 있으며,fooHelper()
메소드를 통해 실제 요소에 접근하고 수정하는 작업을 수행할 수 있습니다.
- WildcardError 예제는 컴파일 시 캡처 오류를 생성합니다.
✅ Zoo를 예시로 든 Wildcard에 대한 예시 설명
우리는 Animal
이라는 클래스를 상속하는 여러 동물 클래스가 있습니다. 그리고 Zoo
라는 클래스에서는 Animal
을 상속하는 동물들을 관리하는 리스트를 사용합니다. 다음은 간단한 코드 예시입니다
import java.util.ArrayList;
import java.util.List;
class Animal { }
class Lion extends Animal { }
class Tiger extends Animal { }
class Zoo {
void printAnimals(List<Animal> animals) {
for (Animal animal : animals) {
System.out.println(animal);
}
}
}
여기서 Zoo
클래스의 printAnimals
메서드는 Animal
객체들의 리스트를 인자로 받아서 출력합니다. 이때, List<Animal>
은 Animal
타입의 리스트만 받을 수 있습니다.
이제 우리가 Lion
과 Tiger
객체를 각각 가지고 있는 리스트를 출력하고 싶다고 가정해봅시다. 다음과 같은 코드를 작성해보겠습니다.
public class Main {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Lion());
animals.add(new Tiger());
Zoo zoo = new Zoo();
zoo.printAnimals(animals);
}
}
위 코드는 정상적으로 동작하며, Lion
과 Tiger
객체가 Animal
타입의 리스트에 저장되어 출력됩니다. 이는 Lion
과 Tiger
가 Animal
의 하위 타입이기 때문에 가능합니다.
그러나 이번에는 Zoo
클래스의 printAnimals
메서드에 Lion
과 Tiger
객체를 가지고 있는 리스트를 전달하고 싶다고 가정해봅시다. 즉, 다음과 같은 코드를 작성하려고 합니다.
public class Main {
public static void main(String[] args) {
//Lion 객체를 가지는 List
List<Lion> lions = new ArrayList<>();
lions.add(new Lion());
Zoo zoo = new Zoo();
//Lion 객체를 가지는 List를 인자로 메소드에 넣음
//void printAnimals(List<Animal> animals) 이와 같이 선언되어 있다.
zoo.printAnimals(lions); // 컴파일 에러
}
}
위 코드는 컴파일 시에 에러가 발생합니다. 이유는 Zoo
클래스의 printAnimals
메서드에서 List<Animal>
을 인자로 요구하는데, 우리는 List<Lion>
을 전달하려고 했기 때문입니다. 이는 타입 불일치로 인한 컴파일 에러가 발생하는 것입니다. List<Animal> !⊃ List<Lion>, List<Animal>은 List<Lion>을 포함하지 않습니다. 즉, 상속 관계가 아닙니다.
이런 상황에서 와일드카드(?
)를 사용하면 유연성을 얻을 수 있습니다. 다음은 Zoo
클래스를 와일드카드를 사용하여 수정한 예시입니다.
class Zoo {
void printAnimals(List<? extends Animal> animals) {
for (Animal animal : animals) {
System.out.println(animal);
}
}
}
이제 printAnimals
메서드의 매개변수 타입은 List<? extends Animal>
입니다. 이는 Animal
을 상속하는 어떤 타입의 리스트도 받을 수 있다는 의미입니다. 이는 컴파일러가 와일드 카드(”?”)에 대한 타입을 어느 정도 추론하여 판단할 수 있는 정보를 제공합니다. 따라서, 이제 우리는 List<Lion>
을 전달해도 컴파일 에러가 발생하지 않습니다. 예를 들어, 다음과 같은 코드가 동작합니다.
public class Main {
public static void main(String[] args) {
List<Lion> lions = new ArrayList<>();
lions.add(new Lion());
Zoo zoo = new Zoo();
zoo.printAnimals(lions); // 컴파일 에러 없음
}
}
위 코드는 정상적으로 동작하며, Lion
객체를 가지고 있는 List
를 printAnimals
메서드에 전달하여 출력할 수 있습니다. 이는 와일드카드(?
)를 사용하여 List
의 요소 타입을 더 유연하게 처리할 수 있기 때문에 가능합니다.
✅ Guidelines for Wildcard Use
- 와일드 카드 사용 지침
제네릭으로 프로그래밍하는 방법을 배울 때 더 혼란스러운 측면 중 하나는 upper bounded 와일드카드를 사용할 시기와 lower bounded 와일드카드를 사용할 시기를 결정하는 것입니다. 이 페이지에서는 코드를 디자인할 때 따라야 할 몇 가지 지침을 제공합니다.
- An "In" Variable
"in" 변수는 코드에 데이터를 제공합니다. copy(src, dest)라는 두 개의 아규먼트가 있는 복사 메서드를 상상해 보십시오. src 아규먼트는 복사할 데이터를 제공하므로 "in" 파라미터입니다.
- An "Out" Variable
"out" 변수는 다른 곳에서 사용할 데이터를 보유합니다. 복사 예제에서 copy(src, dest), dest 아규먼트는 데이터를 허용하므로 "out" 파라미터입니다.
- ➡️ Wildcard Guidelines
- "in" 변수는 extends 키워드를 사용하여 upper bounded 와일드카드로 정의됩니다.
- "out" 변수는 super 키워드를 사용하여 lower bounded 와일드카드로 정의됩니다.
- Object 클래스에 정의된 메서드를 사용하여 "in" 변수에 액세스할 수 있는 경우 unbounded 와일드카드를 사용합니다.
- 코드가 "in" 및 "out" 변수로 변수에 액세스해야 하는 경우 와일드카드를 사용하지 마십시오.
- An "In" Variable
- 예시
List<? extends ...>로 정의된 리스트는 비공식적으로 읽기 전용으로 간주될 수 있지만 이것이 엄격하게 보장되는 것은 아닙니다. 다음 두 클래스가 있다고 가정합니다.
class NaturalNumber { private int i; public NaturalNumber(int i) { this.i = i; } // ... } class EvenNumber extends NaturalNumber { public EvenNumber(int i) { super(i); } // ... }
List<EvenNumber> le = new ArrayList<>(); List<? extends NaturalNumber> ln = le; ln.add(new NaturalNumber(35)); // compile-time error
List<EvenNumber>는 List<? extends NaturalNumber>의 subtype이기 때문에, ln에 le를 할당할 수 있습니다. 그러나 ln을 사용하여 짝수 리스트에 NaturalNumber를 추가할 수는 없습니다. 이 리스트에서 다음 작업이 가능합니다.
- null을 추가할 수 있습니다.
- clear를 호출할 수 있습니다.
- iterator을 얻을 수 있고 remove를 호출할 수 있습니다.
- wildcard을 캡쳐할 수 있고 write elements that you've read from the list.
List<? extends NaturalNumber>에 의해 정의된 list는 엄밀한 의미에서 읽기 전용이 아니지만 목록에서 새 요소를 저장하거나 기존 요소를 변경할 수 없기 때문에 그렇게 생각할 수 있습니다.
👉 위의 코드에서 List<? extends NaturalNumber> ln = le;
라인은 와일드카드를 사용하여 ln
변수가 NaturalNumber
또는 그 하위 타입의 리스트를 참조할 수 있도록 합니다. 따라서 le
리스트를 ln
에 할당할 수 있습니다. 그러나 ln
변수가 ? extends NaturalNumber
로 선언되었기 때문에, 컴파일러는 ln
이 가리키는 실제 타입을 알 수 없게 됩니다. 이로 인해 ln
을 통해 리스트에 새 요소를 추가하려고 할 때 컴파일 시간 오류가 발생합니다.
이는 와일드카드의 한 가지 제한 때문입니다. 와일드카드 타입을 사용하면 해당 타입의 요소를 읽는 것은 가능하지만, 쓰기(추가 또는 변경)는 허용되지 않습니다. 이는 와일드카드의 불변성(invariance)으로 인해 발생하는 제한입니다. 따라서 "새 요소를 저장하거나 기존 요소를 변경할 수 없다"는 말은, ln
변수를 통해 리스트에 새로운 요소를 추가하거나 기존 요소를 변경할 수 없다는 의미입니다. ln.add(new NaturalNumber(35));
코드는 컴파일 시간 오류가 발생하게 됩니다.
"읽기 전용"이라는 표현은 와일드카드를 사용한 리스트에서 요소를 추가하거나 변경하는 것이 제한되기 때문에 사용되었습니다. 와일드카드를 사용한 리스트는 읽기 작업(요소의 값을 읽거나 리스트를 반복하는 등)은 가능하지만, 추가 또는 변경 작업은 허용되지 않습니다. 이러한 제한은 타입 안정성을 보장하고 불변성을 유지하기 위한 것입니다.
🏷️이미지 출처 및 참고한 사이트
Uploaded by N2T