CodeGym /행동 /JAVA 25 SELF /Generics wildcards

Generics wildcards

JAVA 25 SELF
레벨 27, 레슨 4
사용 가능

1. 제네릭의 불변성: List<Number> ≠ List<Integer>

Java의 제네릭은 엄격합니다. 즉, 불변적입니다. 이는 한 타입이 다른 타입의 하위 타입이라 하더라도(예: Integer는 Number의 하위 타입) 이 타입들을 담는 컬렉션은 서로 호환되지 않는다는 뜻입니다.

상상해 보세요. 사과 상자(List<Integer>)가 있는데 누군가 이걸 “과일 상자”(List<Number>)라고 말합니다. 겉보기에는 맞아 보입니다. 사과는 과일이니까요. 하지만 그렇게 되면 그 상자에 바나나(Double)도 넣을 수 있게 되고, 실제로는 “사과 상자”였던 것이 망가지게 됩니다.

코드 예:

List<Integer> intList = new ArrayList<>();
List<Number> numList = intList; // 컴파일 오류!

컴파일러는 의도적으로 엄격합니다. “사과 리스트”를 “과일 리스트”로 바꾸지 못하게 막습니다. 그렇지 않으면 무엇이든 넣을 수 있게 되어, 프로그램은 실행 중에야 문제를 드러낼 것입니다.

따라서 List<Number>와 List<Integer>는 서로 전혀 다른 타입입니다. 비록 Integer 자체는 Number의 하위 타입이지만요.

배열과의 차이:

Integer[] intArr = new Integer[10];
Number[] numArr = intArr; // 허용됨(배열은 공변적)
numArr[0] = 3.14; // 런타임 오류(ArrayStoreException)

결론: 제네릭은 컴파일 타임의 타입 안전성을 보장하기 위해 불변적입니다.

2. 타입 매개변수의 경계

때로는 제네릭 클래스나 메서드가 다룰 수 있는 타입을 제한해야 합니다. 이를 위해 경계(bounds)를 사용합니다.

상한 경계(extends)

class Stats<T extends Number> {
    private T[] nums;
    // ...
}

이제 Stats는 Number의 하위 타입들(Integer, Double, Float 등)과만 함께 동작할 수 있습니다. Stats<String>을 생성하려고 하면 컴파일 오류가 발생합니다.

인터페이스로 제한하기

class Sorter<T extends Comparable<T>> {
    void sort(List<T> list) { /* ... */ }
}

이제 Sorter는 Comparable 인터페이스를 구현한 타입에서만 동작합니다.

복수 경계

&로 여러 제한을 동시에 지정할 수 있습니다:

class MyClass<T extends Number & Comparable<T>> { /* ... */ }

T는 Number를 상속하고 Comparable<T>을 구현해야 합니다.

순서가 중요합니다: 클래스가 먼저, 그 다음 인터페이스들입니다.

3. 와일드카드 소개: ?

와일드카드는 “여기에 어떤 타입이 올 수는 있지만, 정확히 무엇인지는 모른다”라는 의미의 대체 타입입니다.

예시:

  • List<?> — 아무 타입의 요소나 담을 수 있는 리스트.
  • List<? extends Number> — Number의 하위 타입(Integer, Double 등)의 리스트.
  • List<? super Integer> — Integer의 상위 타입(Integer, Number, Object) 리스트.

왜 와일드카드가 필요할까요?

서로 관련된 다양한 타입의 컬렉션과 함께 동작하는 메서드를 작성할 수 있게 해줍니다.

예: 읽기 전용(producer)

void printNumbers(List<? extends Number> list) {
    for (Number n : list) {
        System.out.println(n);
    }
    // list.add(123); // 오류! 요소를 추가할 수 없습니다
}
  • Number로 요소를 읽을 수 있습니다.
  • 추가는 할 수 없습니다(null 제외).

예: 쓰기 전용(consumer)

void addIntegers(List<? super Integer> list) {
    list.add(42); // OK
    // Integer x = list.get(0); // 오류! 어떤 타입이 반환될지 알 수 없습니다
}
  • Integer(또는 그 하위 타입)의 요소를 추가할 수 있습니다.
  • 안전하게 Integer로 읽을 수는 없습니다(읽을 수 있는 타입은 Object뿐).

PECS 규칙

PECS — “Producer Extends, Consumer Super”:

  • Producer — Extends: 컬렉션이 데이터를 “생산”(제공)만 한다면(producer) ? extends T를 사용하세요.
  • Consumer — Super: 컬렉션이 데이터를 “소비”(받기)만 한다면(consumer) ? super T를 사용하세요.

기억법: extends — 읽기 전용(producer), super — 쓰기 전용(consumer).

비교: 배열은 공변, 제네릭은 불변

  • 배열은 공변: Integer[]를 Number[] 변수에 대입할 수 있습니다.
  • 제네릭은 불변: List<Integer>를 List<Number> 변수에 대입할 수 없습니다.

와일드카드는 제네릭의 불변성을 부분적으로 완화해 줍니다.

5. 제네릭 메서드와 타입 추론; 제약(type erasure)

제네릭 메서드

어떤 타입과도 동작하는 제네릭 메서드를 선언할 수 있습니다:

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

타입 추론(type inference)

Java는 타입 매개변수를 추론할 수 있습니다:

List<String> strings = List.of("a", "b");
printList(strings); // T = String

제네릭의 제약: 타입 소거

  • Java의 제네릭은 타입 소거로 구현됩니다. 즉, 컴파일 이후 타입 매개변수 정보가 제거됩니다.
  • 바이트코드에서는 List<String>과 List<Integer>의 차이가 없습니다.
  • 제네릭 타입의 배열을 만들 수 없습니다: new List<String>[10] — 오류.
  • 타입 매개변수가 있는 instanceof는 사용할 수 없습니다: obj instanceof List<String> — 오류.

6. 컬렉션과 Stream API 실습

리스트 간 요소 복사

public static <T> void copy(List<? super T> dest, List<? extends T> src) {
    for (T item : src) {
        dest.add(item);
    }
}
  • src — producer(? extends T)
  • dest — consumer(? super T)

사용 예:

List<Integer> ints = List.of(1, 2, 3);
List<Number> nums = new ArrayList<>();
copy(nums, ints); // OK: Number는 Integer의 상위 타입

Stream API와 와일드카드

List<Integer> ints = List.of(1, 2, 3);
List<? extends Number> numbers = ints;

numbers.stream()
    .map(Number::doubleValue)
    .forEach(System.out::println);

와일드카드로 필터링

public static void printAll(List<?> list) {
    for (Object o : list) {
        System.out.println(o);
    }
}

7. 자주 하는 실수

실수 1: Raw type(로 타입).
raw type을 사용하면 타입 검사가 비활성화되어 런타임 오류로 이어질 수 있습니다.

List list = new ArrayList(); // raw type — 좋지 않음!
list.add("문자열");
list.add(123); // 아무거나 추가할 수 있음

String s = (String) list.get(1); // ClassCastException!

raw type을 사용하지 마세요. 항상 타입 매개변수를 지정하세요: List<String>, List<Integer>.

실수 2: extends에서의 안전하지 않은 변환.
? extends ... 컬렉션에 요소를 추가하려 하면 컴파일 오류가 발생합니다.

List<? extends Number> nums = new ArrayList<Integer>();
nums.add(3.14); // 컴파일 오류!

실수 3: 오버로드 시 타입 소거로 인한 충돌.
제네릭 매개변수만 다르게 해서 메서드를 오버로드할 수 없습니다 — 타입 소거 후 시그니처가 동일해집니다.

public void process(List<String> list) { /* ... */ }
public void process(List<Integer> list) { /* ... */ } // 컴파일 오류!

실수 4: 제네릭 타입 배열.
타입 소거 때문에 매개변수화된 타입의 배열을 만들 수 없습니다.

List<String>[] arr = new List<String>[10]; // 컴파일 오류!
1
과제
JAVA 25 SELF, 레벨 27, 레슨 4
잠금
자산 총가치 계산 💰
자산 총가치 계산 💰
1
과제
JAVA 25 SELF, 레벨 27, 레슨 4
잠금
범용 이벤트 로깅 시스템 📜
범용 이벤트 로깅 시스템 📜
1
설문조사/퀴즈
컬렉션 인터페이스, 레벨 27, 레슨 4
사용 불가능
컬렉션 인터페이스
컬렉션 인터페이스
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION