본문 바로가기
Java Tutorials

#18 Lesson: Generics 3

by xogns93 2024. 7. 29.

Wildcards

일반 코드에서 와일드카드라고 하는 물음표(?)는 알 수 없는 타입을 나타냅니다. 와일드카드는 다양한 상황에서 사용할 수 있습니다. 때로는 리턴 타입으로 사용됩니다(좀 더 구체적으로 프로그래밍하는 것이 더 나은 방법이긴 하지만).

와일드카드는 제네릭 메서드 호출, 제네릭 클래스 인스턴스 생성 또는 상위 타입에 대한 타입 아규먼트로 사용되지 않습니다.

다음 섹션에서는

  • 상한 와일드카드(Upper Bounded Wildcards)
  • 하한 와일드카드(Lower Bounded Wildcards)
  • 와일드카드 캡처(Wildcards Capture)

를 포함하여 와일드카드에 대해 자세히 설명합니다.

 

Upper Bounded Wildcards

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를 사용하여 변수에 대한 제한을 완화할 수 있습니다" 내용을 이해할 수 있음)

<T extends Number>와 <? extends Number>의 차이점은?
전자의 경우는 T는 전자의 타입 파라미터 제한에 적합한 타입 아규먼트[Number, Byte, Integer, Long, Short, Double, Float 중에 하나]가 제공되면, T는 특정 타입[Number를 상속한 클래스 타입 중 하나]으로 특정되지만,

후자는 Number를 상속한 subclass들 중, 하나로 특정되지 않고, 타입 아규먼트로
Number, Byte, Integer, Long, Short, Double, Float 클래스와 같은 subclass들이 될 수 있는 것을 의미합니다.

<T extends Number>
  정의: <T extends Number>는 제네릭 타입 파라미터 T가 Number 클래스 또는 그 하위 클래스여야 한다는 것을 의미합니다.
  특징:
    타입 파라미터 선언: 제네릭 클래스나 메서드에서 사용됩니다.
    특정 타입: T는 특정 타입으로 결정됩니다. 예를 들어, T는 Integer, Double, Float 등 Number를 상속한 클래스 중,
                     하나로 결정됩니다.
    타입 안정성: 클래스나 메서드 내부에서 T를 특정 타입으로 안전하게 사용할 수 있습니다.
class Box<T extends Number> {
    private T value;

    public Box(T value) {
        this.value = value;
    }

    public T getValue() {
        return value;
    }
}
위의 예시에서, Box<Integer>, Box<Double> 등으로 사용할 수 있으며, T Integer 또는 Double로 특정됩니다.

<? extends Number>
  정의: <? extends Number>는 widlcard 타입으로, Number 클래스 또는 그 하위 클래스여야 한다는 것을 의미합니다.
  특징:
    와일드카드 사용: 메서드 파라미터나 컬렉션의 타입으로 사용됩니다.
    불특정 타입: ?는 특정되지 않은 타입을 나타내며, Number의 하위 클래스들(Integer, Double, Float 등) 중,
                        하나일 수 있습니다.
    유연성: 와일드카드 타입은 유연하지만, 읽기 전용으로 사용되는 경우가 많습니다. 즉, 값을 추가하는 등의 작업에는
                 제한이 있습니다.
public void processNumbers(List<? extends Number> numbers) {
    for (Number number : numbers) {
        System.out.println(number);
    }
}
위의 예시에서, processNumbers 메서드는 List<Integer>, List<Double>  Number를 상속하는 어떤 타입의 리스트도 받을 수 있습니다. 즉, ?는 Number를 상속한 어떤 서브클래스도 될 수 있습니다.

차이점 요약
1. 타입 파라미터 vs 와일드카드:
   <T extends Number>: 제네릭 타입 파라미터를 선언할 때 사용되며, T는 특정 타입(Integer, Double 등)으로 결정됩니다.
   <? extends Number>: 와일드카드 타입으로, 특정되지 않은 Number의 서브클래스(Integer, Double 등) 중 하나를 의미합니다.
2. 타입 안정성 vs 유연성:
   <T extends Number>: T는 특정 타입이므로, 타입 안전성을 제공합니다. 예를 들어, Box<Integer>와 같이 사용됩니다.
   <? extends Number>: 특정되지 않은 타입이므로 유연성을 제공하지만, 일반적으로 읽기 전용으로 사용됩니다.
3. 사용 위치:
   <T extends Number>: 제네릭 클래스나 메서드의 정의에서 사용됩니다.
   <? extends Number>: 메서드 파라미터나 컬렉션 타입에서 사용됩니다.

따라서, <T extends Number>는 제네릭 타입 파리미터를 특정 타입으로 제한하고, <? extends Number>는 특정되지 않은 Number의 하위 타입을 유연하게 받을 수 있다는 차이가 있습니다.

 

다음 process 메서드에 대해 고려해봅니다:

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

 

upper bounded wildcard, <? extends Foo>, 여기서 Foo는, Foo 및 Foo의 모든 하위 타입과 일치합니다. process 메서드는 Foo 타입으로 List의 엘리먼트에 액세스할 수 있습니다.

import java.util.ArrayList;
import java.util.List;

class Foo {
    private String name;

    public Foo(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

class Bar extends Foo {
    public Bar(String name) {
        super(name);
    }
}

public class Main {
	
    public static void process(List<? extends Foo> list) {
        for (Foo elem : list) {
            System.out.println("Processing: " + elem.getName());
        }
    }

    public static void main(String[] args) {
        List<Bar> barList = new ArrayList<>();
        barList.add(new Bar("Bar1"));
        barList.add(new Bar("Bar2"));
        barList.add(new Bar("Bar3"));

        process(barList); // Using the process method with a List of Bar
    }
}

 

foreach 절에서 elem 변수는 list의 각 엘리먼트를 반복합니다. 이제 Foo 클래스에 정의된 모든 메서드들은 elem에서 사용할 수 있습니다.

 

sumOfList 메서드는 리스트에 있는 숫자들의 합을 반환합니다::

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을 콘솔에 출력합니다.

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));

 


※ 주의 : 아래 코드를 고려해 보세요

package com.intheeast.jeneric;

import java.util.Arrays;
import java.util.List;

public class Program {
	
	public static void printLists(List<Object> list) {
	    for (Object elem : list)
	        System.out.println(elem + " ");
	    System.out.println();
	}
	
    // List<T extends Number> 파라미터에서 컴파일 에러가 발생함
	public static void printLists(List<T extends Number> list) {
	    for (Object elem : list)
	        System.out.println(elem + " ");
	    System.out.println();
	}	
	
	public static void printList(List<? extends Number> list) {
	    for (Object elem : list)
	        System.out.println(elem + " ");
	    System.out.println();
	}	
	
	public static void main(String ...args) {
		Class clazz; // ->> (raw type)...
		String str;  // ->> 
		
		List<Integer> li = Arrays.asList(1, 2, 3);
		List<Double> ld = Arrays.asList(1.0, 2.0, 3.0);
		List<Float> lf = Arrays.asList(1.0f, 2.0f, 3.0f);
		Program.printList(li);
		Program.printList(ld);
		Program.printList(lf);		
		
		List<String>  ls = Arrays.asList("one", "two", "three");
		//printList(ls);
	}
}

 

 

 


Unbounded Wildcards

unbounded[제한되지 않는, 무제한] 와일드카드 타입은 와일드카드 문자(?)를 사용하여 지정됩니다(예: List<?>). 이를 알 수 없는 타입의 list라고 합니다. unbounded 와일드카드가 유용한 접근 방식인 두 가지 시나리오가 있습니다.

  • Object 클래스에서 제공하는 기능을 사용하여 구현할 수 있는 메서드를 작성하는 경우.
  • 코드가 타입 파라미터에 의존하지 않는 제네릭 클래스의 메서드를 사용하는 경우. (예를 들어 List.size 또는 List.clear. 실제로 Class<?>는 Class<T>의 대부분의 메서드가 T에 의존하지 않기 때문에 자주 사용됩니다)

다음 메서드 printList을 고려해보세요:

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

 

printList의 목표는 모든 타입의 list을 print 하는 것이지만, 이 목표를 달성하는 데 실패했습니다. Object 인스턴스의 list만 print 합니다. List<Integer>, List<String>, List<Double> 등은 List<Object>의 하위 타입이 아니기 때문에 print할 수 없습니다.

Lesson: Generic1의 다음 설명을 떠 올려 보세요
Note: 두 가지 Concrete Type A와 B(예: Number 및 Integer)가 주어지면 MyClass<A>는 A와 B가 관련되어 있는지 여부에 관계없이 MyClass<B>와 관계가 없습니다. MyClass<A> 및 MyClass<B>의 공통 부모는 Object입니다.타입 파라미터가 관련되어 있을 때 두 제네릭 클래스 간에 하위 타입과 유사한 관계를 만드는 방법에 대한 자세한 내용은 와일드카드 및 하위 유형 지정을 참조하세요.

 

 

일반 printList 메소드를 작성하려면 List<?>를 사용하십시오.

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

 

구체적인 타입 A의 경우, List는 List의 하위 타입이므로 printList를 사용하여 모든 타입의 list를 인쇄할 수 있습니다.

List<Integer> li = Arrays.asList(1, 2, 3);
List<String>  ls = Arrays.asList("one", "two", "three");
printList(li);
printList(ls);
Note: Arrays.asList 메서드는 이 단원 전체의 예제에서 사용됩니다. 이 정적 팩터리 메서드는 지정된 배열을 변환하고 고정 크기 list를 반환합니다.

 

 List<Object>와 List<?>는 동일하지 않다는 점에 유의해야 합니다.

List<Object>에 Object 또는 Object의 하위 타입을 삽입할 수 있습니다.


그러나 List<?>에는 null만 삽입할 수 있습니다.

import java.util.ArrayList;
import java.util.List;

public class WildcardExample {
    public static void main(String[] args) {
        List<?> wildcardList = new ArrayList<Integer>();

        // null 삽입은 허용됩니다.
        wildcardList.add(null);

        // 다른 객체 삽입 시도 (컴파일 오류 발생)
        // wildcardList.add("Hello");  // 컴파일 오류
        // wildcardList.add(123);      // 컴파일 오류

        System.out.println("List contains: " + wildcardList);
    }
}

 

위 컴파일 에러가 발생하는 것은 당연한 결과입니다. List는 동일한 데이터 타입의 엘리먼트들을 그룹화하기 때문입니다.


Guidelines for Wildcard Use 에는 주어진 상황에서 어떤 종류의 와일드카드를 사용해야 하는지 결정하는 방법에 대한 자세한 정보가 있습니다.

 

Lower Bounded Wildcards

상한 와일드카드 섹션에서는 상한 와일드카드가 알 수 없는 타입을 특정 타입 또는 해당 타입의 하위 타입으로 제한하고 extends 키워드를 사용하여 표시됨을 보여줍니다.

비슷한 방식으로 Lower Bounded Wildcards는 알 수 없는 타입을 특정 타입 또는 해당 타입의 상위 타입으로 제한합니다.

Lower Bounded Wildcards는 와일드카드 문자(?), super 키워드, lower bound을 차례로 사용하여 표현됩니다.

<? super A>

※ 예를 들어 <? super Integer> 일 경우, Integer 클래스[Integer 클래스 포함]의 상속 계층의 ancestor class들이 ?에 해당합니다.

참고: 와일드카드의 upper bound을 지정하거나 lower bound을 지정할 수 있지만 둘 다 지정할 수는 없습니다.

// 잘못된 코드 예시
public void invalidMethod(List<? extends Number super Integer> list) {
    // ...
}

 

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과 일치하기 때문입니다[is a].

is a 관계
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger;

 

다음 코드는 1에서 10까지의 숫자를 list 끝에 추가합니다.

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

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

    public static void main(String[] args) {
        // Example with List<Integer>
        List<Integer> integerList = new ArrayList<>();
        addNumbers(integerList);
        System.out.println("Integer list: " + integerList);

        // Example with List<Number>
        List<Number> numberList = new ArrayList<>();
        addNumbers(numberList);
        System.out.println("Number list: " + numberList);

        // Example with List<Object>
        List<Object> objectList = new ArrayList<>();
        addNumbers(objectList);
        System.out.println("Object list: " + objectList);

        // Example with List<Comparable>
        List<Comparable> comparableList = new ArrayList<>();
        addNumbers(comparableList);
        System.out.println("Comparable list: " + comparableList);

        // Example with LinkedList<Object>
        List<Object> linkedObjectList = new LinkedList<>();
        addNumbers(linkedObjectList);
        System.out.println("Linked Object list: " + linkedObjectList);
    }
}

 

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를 사용합니다.

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // Create a list of Integer
        List<Integer> integerList = new ArrayList<>();
        integerList.add(1);
        integerList.add(2);
        integerList.add(3);
        integerList.add(4);
        integerList.add(5);

        // Assign integerList to List<? extends Integer>
        List<? extends Integer> intList = integerList;

        // Assign intList to List<? extends Number>
        List<? extends Number> numList = intList;  // OK. List<? extends Integer> is a subtype of List<? extends Number>

        // Read elements from numList
        for (Number num : numList) {
            System.out.println("Number: " + num);
        }

        // Demonstrating that we cannot add elements to intList or numList
        // intList.add(6);  // Compile error: cannot add element to List<? extends Integer>
        // numList.add(6);  // Compile error: cannot add element to List<? extends Number>

        // However, we can still read elements as Number
        Number firstNumber = numList.get(0);
        System.out.println("First number: " + firstNumber);

        // Creating a list of Double to demonstrate more flexibility with Number
        List<Double> doubleList = new ArrayList<>();
        doubleList.add(1.1);
        doubleList.add(2.2);
        doubleList.add(3.3);

        // Assign doubleList to List<? extends Number>
        List<? extends Number> anotherNumList = doubleList;

        // Read elements from anotherNumList
        for (Number num : anotherNumList) {
            System.out.println("Number from anotherNumList: " + num);
        }
    }
}

 

Integer는 Number의 subtype이고 numList는 Number 객체 리스트이므로 이제 intList(Integer 객체 목록)와 numList 간에 관계가 존재합니다. 다음 다이어그램은 upper 및 lower bounded 와일드카드로 선언된 여러 List 클래스 간의 관계를 보여줍니다.

 

 

Wildcard Capture and Helper Methods

 

어떤 경우에는 컴파일러가 와일드카드 타입을 유추합니다.

예를 들어 List는 List<?>로 정의될 수 있지만, expression을 실행할 때 컴파일러는 코드에서 특정 타입을 유추합니다.

이 시나리오를 와일드카드 캡처라고 합니다.

대부분의 경우 capture of라는 문구가 포함된 오류 메시지가 표시되는 경우를 제외하고는 

와일드카드 캡처에 대해 걱정할 필요가 없습니다.

WildcardError 예제는 컴파일 시 캡처 오류를 생성합니다.

import java.util.List;

public class WildcardError {

    void foo(List<?> i) {
        i.set(0, i.get(0));
    }
}

 

이 예제에서 컴파일러는 i 입력 파라미터를 Object 타입으로 처리합니다.

foo 메서드가 List.set(int, E)를 호출하면 컴파일러는 리스트에 삽입되는 객체의 타입을 확인할 수 없으며 오류가 생성됩니다.

이러한 타입의 오류가 발생하면 일반적으로 컴파일러가 여러분이 잘못된 타입을 변수에 지정하고 있다고 믿는다는 의미입니다. 이러한 이유로 제네릭이 Java 언어에 추가되었습니다.

즉, 컴파일 시 타입 안전성을 강화하기 위해서입니다.

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(1)); // 테스트를 위해 0번째 엘리먼트를 1번째 엘리먼트로 set하는 코드로 변경함
    }
    
    public static void main(String[] args) {
        WildcardError wildcardError = new WildcardError();
        
        // Integer 타입의 리스트 생성 및 초기화
        List<Integer> intList = new ArrayList<>();
        intList.add(42);  // 첫 번째 요소
        intList.add(15);  // 두 번째 요소
        
        System.out.println("Before foo: " + intList);
        wildcardError.foo(intList);
        System.out.println("After foo: " + intList);

        // String 타입의 리스트 생성 및 초기화
        List<String> strList = new ArrayList<>();
        strList.add("Hello");  // 첫 번째 요소
        strList.add("World");  // 두 번째 요소
        
        System.out.println("Before foo: " + strList);
        wildcardError.foo(strList);
        System.out.println("After foo: " + strList);
    }

}

 

규칙에 따라 헬퍼 메서드는 일반적으로 originalMethodNameHelper로 이름이 지정됩니다. 

헬퍼 메서드 덕분에 컴파일러는 추론을 사용하여 T가 호출에서 캡쳐 변수인 CAP#1인지 확인합니다. 이제 예제가 성공적으로 컴파일됩니다.

CAP#1은 자바 컴파일러가 제너릭 메서드를 호출할 때 유추한 캡쳐 변수입니다.
캡쳐 변수는 컴파일러가 파라미터화된 타입의 실제 타입을 추론할 때 사용되는 내부적인 변수입니다.
제너릭 메서드를 호출할 때 컴파일러는 유추 과정을 통해 캡쳐 변수를 생성합니다.
이 캡쳐 변수는 제너릭 메서드 내부에서 실제 타입으로 대체되어 사용됩니다.
이를 통해 컴파일러는 호출된 메서드의 타입 아규먼트를 추론하고, 메서드를 올바르게 호출할 수 있도록 합니다. CAP#1은 일반적으로 컴파일러 내부에서 사용되는 임시적인 이름으로, 프로그래머가 직접 사용하거나 참조할 수 없습니다. 컴파일러는 유추한 캡처 변수를 내부적으로 처리하여 제너릭 메서드를 호출하는 데 사용합니다.

 

capture#

capture#는 자바 컴파일러가 내부적으로 사용하는 메커니즘으로, 와일드카드 타입(?)을 다룰 때 발생하는 타입 불확정성을 처리하기 위해 사용됩니다. 이것은 와일드카드에 의해 소개된 "알 수 없는 타입"을 일시적으로 "캡쳐"하고, 각각의 와일드카드 사용에 대해 고유한 참조 타입을 생성하는 과정을 나타냅니다.

Capture란?

  • 와일드카드 타입: List<?>와 같은 와일드카드 타입은 어떤 타입의 요소가 들어갈지 알 수 없습니다.
  • 캡쳐(Capture): 컴파일러는 이 "알 수 없는 타입"을 특정 타입 변수로 "캡쳐"하여 타입 안정성을 보장합니다. 즉, 와일드카드 타입이 사용될 때마다 고유한 참조 타입을 만들어내고, 이를 통해 타입 검사를 수행합니다.
capture#의 이해
1. 와일드카드 사용시 타입 불확정성: 와일드카드 ?를 사용하는 제네릭 타입은 구체적인 타입을 지정하지 않습니다.        이 때문에, 해당 타입이 무엇인지 컴파일러는 알 수 없습니다.
2. 타입 캡쳐 (Type Capture): 컴파일러는 각 와일드카드 사용 시점에 대해 일시적인 참조 타입을 생성합니다.
    이를 타입 캡쳐라고 하며, 이렇게 생성된 타입을, capture#1-of ?, capture#2-of ? 등으로 표현합니다.
    각 capture#는 독립적인 타입을 나타내며, 서로 다른 와일드카드 사용 사례에 대해 서로 다른 타입을 나타냅니다.
3. 타입 안전성 보장: 이러한 메커니즘은 프로그램의 타입 안전성을 보장하기 위해 필요합니다.
    와일드카드를 사용하는 제네릭 타입에 대해 너무 자유롭게 작업을 허용하면, 타입 불일치로 인한
    런타임 오류의 위험이 증가합니다. capture# 메커니즘은 각 와일드카드 사용을 별개의 타입으로 취급하여,
    타입 안전성을 위반할 수 있는 연산을 컴파일 시점에 차단합니다.

 

규칙에 따라 헬퍼 메서드는 일반적으로 originalMethodNameHelper로 이름이 지정됩니다.

위의 WildcardFixed 클래스에서 사용된 해결 방법은 자바의 제네릭 타입 추론 와일드카드 캡처를 활용한 것입니다.

이 방법은 와일드카드를 사용하는 리스트에서 특정 작업을 수행할 때 자주 사용되는 패턴입니다.

 

<해결 방법의 이유>

1. 와일드카드의 제한:

List<?> 타입의 리스트는 "알 수 없는 타입의 엘리먼트를 가지는 리스트"를 의미합니다.

직접적으로 이 리스트에 대해 set 같은 수정 작업을 수행할 수 없습니다.

이는 와일드카드를 사용한 타입의 불확정성 때문입니다.

 

2. 타입 추론을 이용한 Helper 메소드:

fooHelper 메소드는 제네릭 타입 T를 사용하는 List<T>를 매개변수로 받습니다.

이 메소드를 호출할 때,

자바 컴파일러는 전달된 실제 리스트의 타입 아규먼트를 기반으로 T의 구체적인 타입을 추론할 수 있습니다.

foo 메소드에서 fooHelper(i)를 호출하면, 컴파일러는 i의 실제 타입에 대응하는 T를 추론합니다.

이로 인해 와일드카드가 사용된 리스트에 대한 타입 불확정성 문제를 우회할 수 있습니다.

 

3. 타입 안전성 유지:

fooHelper 내에서는 T 타입의 명확한 참조를 가지고 있기 때문에,

리스트의 요소를 안전하게 다룰 수 있습니다. l.set(0, l.get(0)) 구문은

이제 타입 안전성을 위반하지 않으며,컴파일러 에러가 발생하지 않습니다.

 

결론

WildcardFixed 클래스에서의 해결 방법은 와일드카드를 사용하는 리스트에 대해 안전하게 작업을 수행하기 위한

일반적인 패턴입니다.

이는 제네릭 타입 추론과 함께 와일드카드 캡처를 효과적으로 사용하여,

리스트의 요소를 수정하는 작업을 타입 안전하게 만듭니다.

이 패턴은 와일드카드와 제네릭을 사용하는 복잡한 상황에서 타입 안전성을 유지하는 데 매우 유용합니다.

 

이제 더 복잡한 예인 WildcardErrorBad를 살펴보겠습니다.

import java.util.List;

public class WildcardErrorBad {

    void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
      Number temp = l1.get(0);
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
                            // got a CAP#2 extends Number;
                            // same bound, but different types
      l2.set(0, temp);	    // expected a CAP#1 extends Number,
                            // got a Number
    }
}

 

이 예에서 코드는 안전하지 않은 작업을 시도하고 있습니다. 예를 들어 다음과 같은 swapFirst 메서드 호출을 고려하십시오.

List<Integer> li = Arrays.asList(1, 2, 3);
List<Double>  ld = Arrays.asList(10.10, 20.20, 30.30);
swapFirst(li, ld);

 

List<Integer> 및 List<Double>은 모두 List<? extends Number>에서 Integer 값 리스트에서 항목을 가져와서 Double 값 리스트에 배치하려고 시도하는 것은 분명히 올바르지 않습니다.

Oracle의 JDK javac 컴파일러로 코드를 컴파일하면 다음 오류가 발생합니다.

WildcardErrorBad.java:7: error: method set in interface List<E> cannot be applied to given types;
      l1.set(0, l2.get(0)); // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number 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 Number from capture of ? extends Number
WildcardErrorBad.java:10: error: method set in interface List<E> cannot be applied to given types;
      l2.set(0, temp);      // expected a CAP#1 extends Number,
        ^
  required: int,CAP#1
  found: int,Number
  reason: actual argument Number 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 Number from capture of ? extends Number
WildcardErrorBad.java:15: 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 ?
3 errors

 

코드가 근본적으로 잘못되었기 때문에 문제를 해결할 수 있는 헬퍼 메서드가 없습니다. Integer 값 리스트에서 항목을 가져와서 Double 값 리스트에 배치하려고 시도하는 것은 분명히 올바르지 않습니다.

 

 다음 와일드 카드 샘플 코드를 확인해 보겠습니다.

package com.kitec.wildcard1;

import java.util.Arrays;
import java.util.List;

public class ComparisonUtil {
	
//	void foo(List<?> i) {
//        i.set(0, i.get(0)); // 컴파일 에러:
//        					// List의 element가 와일드 카드<?>(알 수 없는 타입 변수)로 
//        					// 설정되었음.
//        					// 그래서 컴파일 타임시, 당연히 에러가 발생함.
//        					// E set(int index, E element);
//                            //   ->2nd Para:E(타입 변수) element
//                            //     : 그러므로 '알 수 없는 타입 변수(?)'와 
//                            //       '요소의 타입을 지정하는 타입 변수(E)' 간에   					//       
//                            //       불일치.
//        					//  와일드 카드 캡처 변수?
//    }
	
	public static <T> void foo(List<T> list) {
	    list.set(0, list.get(0));
	}

	
	public static void printList(List<?> list) {
		// printList(intList); 코드로 인해 printList가 호출되었다면,
		// 실제 디버깅하면 element는 Integer 타입임.
		// : 이는 `와일드 타입 캡처 변수`(실제 컴파일러가 내부적으로 생성하는 임시 변수)에
		//   Object를 Integer로 캡처함
		//   (Integer 타입의 캡처 변수에 element 변수가 캐스팅됨!!!)
	    for (Object element : list) {
	        System.out.println(element.getClass());
	    }
	}
	
//	public static <T> void printList(List<T> list) {
//	    for (T element : list) {
//	        System.out.println(element.getClass());
//	    }
//	}

	
//	void swapFirst(List<? extends Number> l1, List<? extends Number> l2) {
//		Number temp = l1.get(0);
//		l1.set(0, l2.get(0)); 	// expected a CAP#1 extends Number,
//                              	// got a CAP#2 extends Number;
//                              	// same bound, but different types
//		l2.set(0, temp);	  	// expected a CAP#1 extends Number,
//                            	// got a Number
//    }
	
	//public static <T> void compare(List<T> list1, List<T> list2) {
	//public static <T, X> void compare(List<T> list1, List<X> list2) {
	public static void compare(List<?> list1, List<?> list2) {
		
        if (list1.equals(list2)) {
            System.out.println("Equal");
        } else {
            System.out.println("Not Equal");
        }
    }

    public static void main(String[] args) {
        List<String> strList = Arrays.asList("Hello", "World");
        List<Integer> intList = Arrays.asList(1, 2, 3);
        //printList(strList);
        printList(intList);
        
        compare(strList, intList); // Not Equal

//        List<Double> dblList1 = Arrays.asList(3.14, 2.718);
//        List<Double> dblList2 = Arrays.asList(3.14, 2.718);
//        compare(dblList1, dblList2); // Equal
    }
}

 

Wildcards

컬렉션의 모든 엘리먼트를 ​​출력하는 루틴을 작성하는 문제를 생각해 보십시오. 이전 버전의 언어(즉, 5.0 이전 릴리스)에서 작성하는 방법은 다음과 같습니다.

void printCollection(Collection c) {
    Iterator i = c.iterator();
    for (k = 0; k < c.size(); k++) {
        System.out.println(i.next());
    }
}

 

다음은 제너릭을 사용하여 작성한 코드의 초기 버전입니다 (그리고 새로운 for 루프 구문을 사용했습니다):

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

 

문제는 이 새로운 버전이 이전 버전보다 훨씬 유용하지 않다는 것입니다. 이전 코드는 어떤 종류의 컬렉션도 파라미터로 사용할 수 있었지만, 새로운 코드는 Collection<Object>만 받아들이기 때문에 우리가 방금 증명한 대로 이는 모든 종류의 컬렉션의 상위 타입이 아닙니다!

모든 종류의 컬렉션의 상위 타입은 Collection<?> (읽는 방식은 "알 수 없는 엘리먼트의 컬렉션")입니다. 이는 엘리먼트의 타입이 어떤 것이든 상관없이 일치하는 컬렉션을 의미합니다. 이것은 당연한 이유로 와일드카드 타입이라고 불립니다. 다음과 같이 작성할 수 있습니다:

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

 

이제 우리는 어떤 타입의 컬렉션에 대해서도 해당 메서드를 호출할 수 있습니다. printCollection() 내부에서는 여전히 컬렉션 c에서 엘리먼트를 읽고 해당 엘리먼트를 Object 타입으로 간주할 수 있습니다. 이는 항상 안전하며, 실제로 컬렉션의 타입에 관계없이 객체를 포함하고 있기 때문입니다. 그러나 임의의 객체를 추가하는 것은 안전하지 않습니다:

Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error

 

우리는 c의 엘리먼트 타입이 무엇인지 알 수 없기 때문에 객체를 추가할 수 없습니다. add() 메서드는 컬렉션의 엘리먼트 타입인 E 타입의 인자를 받습니다. 실제 타입 파라미터가 ?인 경우, 이는 알 수 없는 타입을 나타냅니다. add에 전달하는 파라미터는 이 알 수 없는 타입의 하위 타입이어야 합니다. 그러나 어떤 타입인지 모르기 때문에 아무 것도 전달할 수 없습니다.

유일한 예외는 null이며, 이는 모든 타입의 멤버입니다.

 이는 모든 타입의 멤버입니다 의 의미는 null은 모든 참조 타입 변수에 할당될 수 있습니다. 즉, null은 어떤 특정 타입의 객체가 아니며, 모든 참조 타입에 대한 디폴트 값이기 때문에 이와 같이 설명하였습니다.

 

반면에 List<?>에서는 get() 메서드를 호출하고 그 결과를 사용할 수 있습니다. 결과 타입은 알 수 없는 타입이지만 항상 객체임을 알 수 있습니다. 따라서 get()의 결과를 Object 타입의 변수에 할당하거나, Object 타입이 예상되는 파라미터로 전달하는 것은 안전합니다.

 

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 파라미터입니다.

 

물론 일부 변수는 in  out 목적으로 모두 사용됩니다. 이 시나리오도 지침에서 다룹니다.

와일드카드 사용 여부와 적절한 와일드카드 타입을 결정할 때 in  out 원칙을 사용할 수 있습니다. 다음 목록은 따라야 할 지침을 제공합니다.


Wildcard Guidelines: 

  • in 변수는 extends 키워드를 사용하여 upper bounded 와일드카드로 정의됩니다.
  • out 변수는 super 키워드를 사용하여 lower bounded 와일드카드로 정의됩니다.
  • Object 클래스에 정의된 메서드를 사용하여 in 변수에 액세스할 수 있는 경우 unbounded 와일드카드를 사용합니다.
  • 코드가 in 및 out 변수로 변수에 액세스해야 하는 경우 와일드카드를 사용하지 마십시오.

이 지침은 메서드의 리턴 타입에 적용되지 않습니다. 리턴 타입으로 와일드카드를 사용하는 것은 이 메서드를 사용하는 프로그래머가 와일드카드를 처리하도록 강제하므로 피해야 합니다.

List<? extends ...>로 정의된 리스트는 비공식적으로 읽기 전용으로 간주될 수 있지만 이것이 엄격하게 보장되는 것은 아닙니다. 다음 두 클래스가 있다고 가정합니다.

class NaturalNumber {

    private int i;

    public NaturalNumber(int i) { this.i = i; }
    // ...
}

class EvenNumber extends NaturalNumber {

    public EvenNumber(int i) { super(i); }
    // ...
}

 

The following code:

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을 사용하여 짝수 List에 NaturalNumber를 추가할 수는 없습니다. 이 List에서 다음 작업이 가능합니다.

  • null을 추가할 수 있습니다.
  • clear를 호출할 수 있습니다.
  • iterator을 얻을 수 있고 remove를 호출할 수 있습니다.
  • wildcard을 캡쳐할 수 있고 write elements that you've read from the list.

List<? extends NaturalNumber>에 의해 정의된 List는 엄밀한 의미에서 읽기 전용이 아니지만 List에서 새 요소를 저장하거나 기존 요소를 변경할 수 없기 때문에 그렇게 생각할 수 있습니다.

'Java Tutorials' 카테고리의 다른 글

#20 Lesson: Packages  (0) 2024.08.02
#19 Lesson: Generics 4  (1) 2024.08.01
#17 Lesson: Generics 2  (1) 2024.07.29
#16 Lesson: Generics 1  (1) 2024.07.28
#15 Lesson: Numbers and Strings [Autoboxing and Unboxing]  (1) 2024.07.28