31. 한정적 와일드카드를 사용해 API 유연성을 높이라
문제 코드
public class Stack<E> {
private List<E> storage;
public void push(E e);
public E pop();
public boolean isEmpty();
//일련의 원소를 스택에 넣는 메서드
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
//Stack안의 모든 원소를 주어진 컬렉션으로 옮겨 담는
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
ArrayList<Integer> integers =
new ArrayList<Integer>(List.of(10, 20, 30));
ArrayList<Double> doubles =
new ArrayList<Double>(List.of(10.3, 20.1, 30.55));
Collection<Object> objects = new ArrayList<>();
numberStack.pushAll(integers);
numberStack.pushAll(doubles);
numberStack.popAll(objects);
}
}
공변성(Covariance) vs 불공변성(Invariance)
구분설명예시허용 여부공변성
하위 타입 관계 유지
Integer[]
→Number[]
⭕
불공변성
타입 관계 무시
List<Integer>
→List<Number>
❌
실행 결과
java: incompatible types: java.lang.Iterable<java.lang.Integer> cannot be converted to java.lang.Iterable<java.lang.Number> java: incompatible types: java.util.Collection<java.lang.Object> cannot be converted to java.util.Collection<java.lang.Number>
해결책 : 한정적 와일드카드 타입
public class Stack<E> {
private List<E> storage;
//...
//일련의 원소를 스택에 넣는 메서드
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
//Stack안의 모든 원소를 주어진 컬렉션으로 옮겨 담는
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
}
핵심 메시지 : 유연성을 극대화하려면 원소의 생산자나 소비자용 입력 매개변수에 와일드카드 타입을 사용하라
PECS (Producer - Extends , Consumer - Super)
와일드카드 타입
종류문법용도예시비한정적
<?>
모든 타입 허용 (타입 제한 없음)
List<?> list = anyList;
한정적 extends
<? extends T>
생산자(Producer): T의 하위 타입
pushAll(Iterable<? extends E> src)
한정적 super
<? super T>
소비자(Consumer): T의 상위 타입
popAll(Collection<? super E> dst)
생산자는 <? extends E> 를 사용하라
소비자는 <? super E> 를 사용하라
PECS 공식은 와일드카드 타입을 사용하는 기본 원칙.
생산자 예시1
public class Chooser<T> {
private final List<T> choiceArray;
// public Chooser(Collection<T> choices) {
public Chooser(Collection<? extends T> choices) {
choiceArray = new ArrayList<>(choices);
}
//...
}
생산자 예시2
//s1과 s2 모두 E의 생산자
public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
Set<E> result = new HashSet<>(s1);
result.addAll(s2);
return result;
}
Set<Integer> integers = Set.of(1, 3, 5);
Set<Double> doubles = Set.of(2.0, 4.0, 6.0);
Set<Number> numbers = union(integers, doubles);
//자바 7 이하
// Set<Number> numbers = Union.<Number>union(integers, doubles);
예시3 - Comparable을 구현한 E
//public static <E extends Comparable<E>> E max(List<E> list)
public static <E extends Comparable<? super E>> E max(List<? extends E> List) {
if (list.isEmpty())
throw new IllegalArgumentException("컬렉션이 비어 있습니다");
E result = null;
for (E e : list)
if (result == null || e.compareTo(result) > 0)
result = Object.requireNonNull(e);
return result;
}
public static <E extends Comparable<E>> E max(List<E> list)
public static <E extends Comparable<? super E>> E max(List<? extends E> List)
List<E> list
->List<? extends E> list
list는 인스턴스 E를 생산
Comparable<E>
->Comparable<? super E>
Comparable는 E 인스턴스를 소비
(선후관계를 뜻하는 정수 반환)
Comparable은 언제나 소비자
일반적으로
Comparable<E> 보다는 Comparable<? super E>를 사용하는 편이 낫다.
Comparator도 마찬가지
예시 3-2
수정된 max 활용
List<ScheduledFuture<?>> scheduledFutures = ...;
public interface Comparable<E>
public interface Delayed extends Comparable<Delayed>
public interface ScheduledFuture<V> extends Delayed, Future<V>
public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
List<ScheduledFuture<?>> scheduledFutures = new ArrayList<>();
ScheduledFuture<?> future1 = scheduler.schedule(() ->
System.out.println("Task 1"), 3, TimeUnit.SECONDS);
scheduledFutures.add(future1);
ScheduledFuture<?> future2 = scheduler.schedule(() ->
System.out.println("Task 2"), 1, TimeUnit.SECONDS);
scheduledFutures.add(future2);
ScheduledFuture<?> future3 = scheduler.schedule(() ->
System.out.println("Task 3"), 5, TimeUnit.SECONDS);
scheduledFutures.add(future3);
try {
// 핵심
ScheduledFuture<?> maxFuture = max(scheduledFutures);
System.out.println("가장 긴 지연 시간을 가진 작업 (예상): " + maxFuture);
for (ScheduledFuture<?> future : scheduledFutures) {
long delay = future.getDelay(TimeUnit.MILLISECONDS);
System.out.println(future + " 남은 지연 시간: " + delay + " ms");
}
long maxDelay = maxFuture.getDelay(TimeUnit.MILLISECONDS);
System.out.println("maxFuture의 남은 지연 시간: " + maxDelay + " ms");
} catch (IllegalArgumentException e) {
System.err.println("Error: " + e.getMessage());
}
scheduler.shutdown();
scheduler.awaitTermination(6, TimeUnit.SECONDS);
}
예시4 - 타입 캡처링 기법
swap 메서드의 두 가지 선언
//1. 비한정적 타입매개변수
public static <E> void swap(List<E> list, int i, int j);
//2. 비한정적 와일드카드
public static void swap(List<?> list, int i, int j);
기본 규칙 : 메서드 선언에 타입 매개변수가 한 번만 나오면 와일드 카드로 대체하라.
비한정적 타입 매개변수 -> 비한정적 와일드카드
// E는 메서드 선언에서 List<E>의 타입 인수로 딱 한 번만 사용 public static <E> void printFirst(List<E> list) { if (!list.isEmpty()) System.out.println(list.get(0)); }
public static void printFirst(List<?> list)
한정적 타입 매개변수 -> 한정적 와일드카드
// E는 List<E>와 E extends Number에서 사용 public static <E extends Number> boolean areAllNumbers(List<E> list) { for (E element : list) { // element는 Number 또는 Number의 하위 타입 } return true; }
public static boolean areAllNumbers(List<? extends Number> list)
문제점
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
오류
java: incompatible types: java.lang.Object cannot be converted to capture#1 of ?
컴파일러는 list가 실제로 어떤 타입의 객체를 담고 있는지 정확히 알 수 없음
List<?> 에는 null만 추가 가능
타입 캡처링
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 도우미 메서드
private static <E> void swapHelper(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
비록 list가 List<?>로 선언되었지만, 이 특정 list 인스턴스는 런타임에 어떤 구체적인 타입을 획득
컴파일러는 이 알 수 없는 구체적인 타입을 swapHelper 메서드의 타입 매개변수 E로 캡처
컴파일러가 E를 캡처된 실제 타입으로 간주하므로, swapHelper 내부에서는 list를 List처럼 안전하게 처리
핵심 정리
조금 복잡하더라도 와일드카드 타입을 적용하면 API가 훨씬 유연해진다.
그러니 널리 쓰일 라이브러리를 작성한다면 반드시 와일드카드 타입을 적절히 사용해줘야 한다.
PECS 공식을 기억하자.
즉, 생산자는 extends를 소비자는 super를 사용한다.
Comparable과 Comparator는 모두 소비자라는 사실도 잊지 말자.
스스로 질문 & 그 외
<? extends E>
직관적으로 봤을때, E를 상속한 클래스만 허용되는 것 같다?
<? extends E>
직관적으로 봤을때, E를 상속한 클래스만 허용되는 것 같다?상속의 의미를 엄격하게 해석하면 클래스가 자기 자신을 상속하는 것은 아니므로 "extends"라는 키워드가 완벽하게 들어맞지는 않는다는 점을 지적
자기 자신도 포함합니다
<? super E>
또한 마찬가지
입력 매개변수가 생산자와 소비자 역할을 동시에 한다면?
타입을 정확히 지정해야 하는 상황으로, 이떄는 와일드카드 타입을 쓰지 말아야 한다
<E extends Comparable<? super E>>
주의사항?
<E extends Comparable<? super E>>
주의사항?타입 계층 구조 내에서 Comparable 구현이 일관성 있고 안전하게 이루어진다면 상위 및 하위 타입 간의 비교에 문제가 없을 것이라는 합리적인 가정을 바탕으로 설계된 것
ScheduledFuture란?
Java의 동시성 유틸리티에서 제공하는 인터페이스
미래에 특정 시간 이후에 실행될 작업의 결과를 나타냄
Future 인터페이스 확장:
비동기 작업의 결과를 관리하는 기능 제공
작업을 취소, 작업이 완료 확인, 작업 결과 반환 가능
결과를 얻을 때까지 대기 가능
Delayed 인터페이스 확장:
작업이 실행될 때까지 남은 지연 시간 확인 가능
지연 시간을 기준으로 다른 예약된 작업과 비교가능
Comparable 인터페이스 구현
예시4 클라이언트에서 사용할 때 차이?
private static <E> void swapHelper(List<?> list, int i, int j)
public static void swap(List<?> list, int i, int j)
public static <E> void swap(List<E> list, int i, int j)
이 메서드를 호출할 때는 특정 타입의 List를 인수로 전달
컴파일러는 전달된 List의 타입을 기반으로 E의 타입을 추론
List<String> names = new ArrayList<>(List.of("Alice", "Bob")); SwapExample.swap(names, 0, 1); // 컴파일러는 E를 String으로 추론 List<Integer> numbers = new ArrayList<>(List.of(1, 2)); SwapExample.swap(numbers, 0, 1); // 컴파일러는 E를 Integer로 추론
제약 사항
클라이언트는 타입이 명확하게 정의된 List를 사용해야 합니다.
List<?> 타입의 변수를 직접 전달하려고 하면 컴파일러가 E의 타입을 추론할 수 없어 에러가 발생할 수 있습니다.
List<?> unknownList = new ArrayList<>(List.of(1, "two")); // SwapExample.swap(unknownList, 0, 1); // 컴파일 에러 발생 (타입 추론 불가
public static void swap(List<?> list, int i, int j)
어떤 타입의 List라도 인수로 전달 가능
List의 요소 타입이 구체적으로 알려지지 않은 List<?> 타입의 변수도 문제없이 전달 가능
SwapExample.swap(names, 0, 1); // List<String>은 List<?>로 업캐스팅 가능 SwapExample.swap(numbers, 0, 1); // List<Integer>은 List<?>로 업캐스팅 가능 List<?> unknownList = new ArrayList<>(List.of(1, "two")); SwapExample.swap(unknownList, 0, 1); // 정상적으로 호출 가능
유연성: 이 버전은 타입에 대한 제약이 적어 더 많은 상황에서 사용할 수 있습니다.
Last updated