[Effective Java] 반환 타입으로는 스트림보다 컬렉션이 낫다

들어가며

스트림은 반복을 지원하지 않는다. Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메소드를 전부 포함할 뿐만 아니라 Iterable 인터페이스가 정의한 방식대로 동작한다. 하지만 Stream이 Iterable을 extend하지 않아서 반복할 수 없다. 이럴 경우 Stream으로 반복을 위한 어뎁터를 사용해야 한다. 반대로 Iterable만 반환하면 스트림 파이프라인에서 처리할 수가 없다. 이 또한 역으로 어뎁터를 만들어야 한다. 다음은 어뎁터에 해당하는 코드이다.

1
2
3
4
5
6
7
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);
}

하지만 API를 작성할 때는 스트림 파이프라인을 사용하려는 사용자와 반복문에서 사용하려는 사용자를 모두 배려해야한다. 이럴때는 Collection 인터페이스를 사용하면 된다. Collection 인터페이스는 Iterable의 하위 타입이고 stream 메소드도 제공하기 때문이다. 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는게 일반적으로 최선이다. 하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올리면 안된다. 반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는 편이 좋다. 다음은 주어진 집합의 멱집합을 반환하는 컬렉션이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
if(src.size() > 30)
throw new IllegalArgumentException("maximum elements must be under 30");

return new AbstractList<Set<E>>() {
@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;
}

@Override
public int size() {
return 1 << src.size();
}

@Override
public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set) o);
}
};
}

위 코드에서 get 메소드의 index의 각각의 비트는 자연스럽게 멱집합의 원소 인덱스와 맵핑된다. 이는 스트림으로도 작성할 수 있다. 위 코드보다 메모리 사용량은 많지만 간단히 작성할 수 있는 장점이 있다.

1
2
3
4
5
6
7
8
9
10
11
public static <E> Stream<List<E>> of(List<E> list) {
return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list).flatMap(main::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()));
}

위 코드는 다음과 같은 반복문 코드와 취지가 비슷하다.

1
2
3
for (int i = 0; i < l.size(); i++)
for(int j = i + 1; j <= l.size(); j++)
System.out.println(l.subList(i,j));

Conclusion

원소 시퀀스를 반환하는 메소드를 작성할 때는 스트림과 iterable을 모두 지원하는 콜렉션을 통해 반환하라. 원소의 개수가 많다면 위의 멱집합과 같은 예 처럼 전용 콜렉션을 만드는것이 방법이다. 콜렉션 반환이 불가능하다면 스트림과 iterable중 자연스러운것을 반환하라.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/16/Languages/Effective%20JAVA/item47/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.