2023. 12. 7. 23:22ㆍBook/이펙티브 자바
아이템 28에서 이야기했듯 매개변수화 타입은 불공변이다.
=> List<type1> != List<type2>
리스코프 치환원칙을 생각해보면 불공변인게 정상이다.
=> List<String>은 List<Object>가 하는 일을 제대로 수행을 하지 못하니 하위 타입이 될 수 없다.
하지만 불공변 방식은 유연하지 못하다. 다음 Stack 예제를 보자
public class Stack<E> {
pubilc Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
여기에 일련의 원소를 스택에 넣는 메서드를 추가해야 한다고 가정하자
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
이 메서드는 스택의 원소 타입과 src의 원소 타입이 일치하면 잘 동작한다.
하지만 Stack의 원소 타입이 Number이고 src의 원소 타입이 Integer라면 타입이 불일치해 컴파일을 하지 못한다.
Number는 Integer의 상위 타입이니 들어와도 전혀 문제가 없고 더 유연하게 사용할 수 있다. 이렇게 쓰려면 한정적 와일드카드 타입이라는 특별한 매개변수를 사용해야 한다.
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
이렇게 메서드의 매개변수를 수정하면 다음과 같은 동작이 가능해진다.
public static void main(String[] args) {
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = Arrays.asList(3, 1, 4, 1, 5, 9);
numberStack.pushAll(integers);
}
popAll 메서드도 보자. 이 메서드는 매개변수에 모든 원소를 담아 리턴하는 메서드이다.
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
이번에도 주어진 컬렉션의 원소 타입이 스택의 원소 타입과 일치한다면 문제없이 동작한다.
하지만 이번에도 유연하지 못한 부분이 있다.
Stack<Number>의 원소를 Collection<Object>로 담지 못한다.
Object는 Number의 상위 타입이니 Number의 원소가 Object 컬렉션에 들어가도 전혀 문제가 없다.
이번에도 와일드 카드가 문제를 해결해준다.
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
'E의 상위 타입의 Collection' 이라는 의미로 더욱 유연하게 API 를 사용할 수 있다.
다음 공식을 외워두면어떤 와일드카드 타입을 써야 하는지 도움이 된다.
-PESC: Producer-Extends, Consuper-Super
Producer-Extends
- Object의 컬렉션 Number나 Integer를 넣을 수 있다.
- Number의 컬렉션에 Integer를 넣을 수 있다.
- 입력 매개변수로부터 원소를 꺼내 컬렉션에서 이용
Consuper-Super
- Integer의 컬렉션의 객체를 꺼내서 Number의 컬렉션에 담을 수 있다.
- Number나 Integer의 컬렉션의 객체를 꺼내서 Object의 컬렉션에 담을 수 있다.
- 컬렉션에서 원소를 꺼내 입력 매개변수에서 이용
책에서는 타입 매개변수와 와일드카드에는 공통되는 부분이 있어서, 메서드를 정의할 때 둘 중 어느 것을 사용해도 괜찮을 때가 많다고 한다.(개인적으로 와일드카드 혼자서만 쓰는건 구현부를 숨긴 것 말고는 큰 의미가 없어보인다...)
List에서 명시한 인덱스의 값을 바꿔주는 정적 메서드를 두 가지 방식으로 정의해보자
public static void swap(List<?> list, int i, int j);
public static <E> void swap(List<E> list, int i, int j);
둘 다 장단이 있다.
먼저 첫 번째는 메서드 시그니처가 간단해 보인다는 점이다. 하지만 문제는 구현부이다.
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
list의 get 메소드는 문제가 없는데 set 메소드는 요청 매개변수 타입을 알 수가 없어 컴파일 에러가난다. 그래서 헬퍼 클래스를 같이 사용해줘야 한다.
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// 와일드카드 타입을 실제 타입으로 바꿔주는 private 도우미 메서드
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
타입 매개변수 메서드 구현부와 비교해보자
public static <E> void swap(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
내 생각엔 굳이 헬퍼 클래스까지 사용해가면서 써야하나? 하는 생각이 든다.
'Book > 이펙티브 자바' 카테고리의 다른 글
Item34 - int 상수 대신 열거 타입을 사용하라(1) (1) | 2023.12.10 |
---|---|
Item32 - 제네릭과 가변인수를 함께 쓸 때는 신중하라 (1) | 2023.12.08 |
Item30 - 이왕이면 제네릭 메서드로 만들라 (1) | 2023.12.06 |
Item29 - 이왕이면 제네릭 타입으로 만들라 (1) | 2023.12.06 |
Item28 - 배열보다는 리스트를 사용하라 (2) | 2023.12.05 |