[배경]
원소 시퀀스, 즉 일련의 원소를 반환하는 메서드는 수없이 많다.
- 자바 7까지는 이런 메서드의 반환 타입으로 컬렉션 인터페이스, Iterable, 배열을 썼다.
- 그런데 자바 8이 스트림이라는 개념을 들고 오면서 반환 타입의 선택이 아주 복잡해졌다.
스트림은 반복(iteration)을 지원하지 않는다.
- 따라서 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.
- 사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작한다.
- 그럼에도 for-each로 스트림을 반복할 수 없는 까닭은 바로 Stream이 Iterable을 확장(extend)하지 않아서다.
어댑터를 사용하면 반환값의 Stream → Iterable, Iterable → Stream 변경이 가능하다.
- 어떤 API가 Stream만 반환하거나 Iterable만 반환한다면 어댑터를 만들어 원하는 반환값을 얻게 할 수 있다.
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
[Why]
공개 API를 작성할 때는 스트림 파이프라인을 사용하는 사람과 반복문에서 쓰려는 사람 모두를 배려해야 한다.
- Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다.
- 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선이다.
어댑터는 클라이언트 코드를 어수선하게 만들고 성능도 안 좋다.
[How]
단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.
- 반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 방안을 검토해보자.
예시) 주어진 집합의 멱집합(power set, 한 집합의 모든 부분집합을 원소로 하는 집합)을 반환하는 경우
- {a, b, c}의 멱집합은 {{}, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}다.
- 원소 개수가 n개면 멱집합의 원소 개수는 2^n개가 된다.
- 따라서 멱집합을 표준 컬렉션 구현체에 저장하려는 생각은 위험하다.
- 하지만 AbstractList를 이용하면 훌륭한 전용 컬렉션을 손쉽게 구현할 수 있다.
public class PowerSet {
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if (src.size() > 30) {
throw new IllegalArgumentException(
"집합에 원소가 너무 많습니다(최대 30개).: " + s);
}
return new AbstractList<Set<E>>() {
@Override
public int size() {
// 멱집합의 크기는 2를 원래 집합의 원소 수만큼 거듭제곱한 것과 같다.
return 1 << src.size();
}
@Override
public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set)o);
}
@Override
public Set<E> get(int index) {
Set<E> result = new HashSet<>();
for (int i=0; index != 0; i++, index >>= 1) {
if ((index & 1) == 1) {
result.add(src.get(i));
}
}
return result;
}
};
}
}
- 비결은 멱집합을 구성하는 각 원소의 인덱스를 비트 벡터로 사용하는 것이다.
- 인덱스의 n번째 비트 값은 멱집합의 해당 원소가 원래 집합의 n번째 원소를 포함하는지 여부를 알려준다.
- 따라서 0부터 2^n - 1까지의 이진수와 원소 n개인 집합의 멱집합과 자연스럽게 매핑된다.
- 예를 들어 {a, b, c}의 멱집합은 원소가 8개이므로 유효한 인덱스는 0~7이며, 이진수로는 000~111이다.
- 이때 인덱스를 이진수로 나타내면, 각 n번째 자리의 값이 각각 원소 a, b, c를 포함하는지 여부를 알려준다.
- 즉, 멱집합의 000번째 원소는 {}, 001번째는 {a}, 101번째는 {c, a}, 111번째는 {c, b, a}가 되는 식이다.
- AbstractCollection을 활용해서 Collection 구현체를 작성할 때는 Iterable용 메서드 외에 2개만 더 구현하면 된다.
- 바로 contains와 size다.
- contains와 size를 구현하는 게 불가능할 때는 컬렉션보다는 스트림이나 Iterable을 반환하는 편이 낫다.
예시) 입력 리스트의 (연속적인) 부분 리스트를 모두 반환하는 메서드를 작성하는 경우
필요한 부분리스트를 만들어 표준 컬렉션에 담는 코드는 단 3줄이면 충분하다.
- 하지만 이 컬렉션은 입력 리스트 크기의 거듭제곱만큼 메모리를 차지한다.
- 기하급수적으로 늘어나는 멱집합보다는 낫지만, 역시나 좋은 방식이 아님은 명백하다.
약간의 통찰만 있으면, 입력 리스트의 모든 부분리스트를 스트림으로 구현하기는 어렵지 않다.
- 첫 번째 원소를 포함하는 부분리스트를 그 리스트의 prefix라 해보자.
- (a, b, c)의 prefix는 (a), (a, b), (a, b, c)가 된다.
- 같은 식으로 마지막 원소를 포함하는 부분리스트를 그 리스트의 suffix라 하자.
- (a, b, c)의 suffix는 (a, b, c), (b, c), (c)가 된다.
- 어떤 리스트의 부분리스트는 단순히 그 리스트의 prefix의 suffix(혹은 suffix의 prefix)에 빈 리스트 하나만 추가하면 된다.
public class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()),
prefixes(list).flatMap(SubLists::suffixes));
}
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
}
- Stream.concat 메서드는 반환되는 스트림에 빈 리스트를 추가하며, flatMap 메서드(아이템45)는 모든 prefix의 모든 suffix로 구성된 하나의 스트림을 만든다.
- 마지막으로 prefix들과 suffix들의 스트림은 IntStream.range와 IntStream.rangeClosed가 반환하는 연속된 정숫값들을 매핑해 만들었다.
- 쉽게 말해 이 관용구는 정수 인덱스를 사용한 표준 for 반복문의 스트림 버전이라 할 수 있다.
for (int start = 0; start < src.size(); start++) {
for (int end = start + 1; end <= src.size(); end++) {
System.out.println(src.subList(start, end));
}
}
- 이 반복문을 그대로 스트림으로 변환할 수 있다.
- 간결해지지만, 읽기에는 더 안 좋을 것이다.
public static <E> Stream<List<E>> of(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start ->
IntStream.rangeClosed(start + 1, list.size())
.mapToObj(end -> list.subList(start, end)))
.flatMap(x -> x);
}
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 49] 매개변수가 유효한지 검사하라 (0) | 2022.03.11 |
---|---|
[아이템 48] 스트림 병렬화는 주의해서 적용하라 (0) | 2022.03.10 |
[아이템 46] 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2022.03.05 |
[아이템 45] 스트림은 주의해서 사용하라 (0) | 2022.03.05 |
[아이템 44] 표준 함수형 인터페이스를 사용하라 (0) | 2022.03.04 |