47. 반환 타입으로는 스트림보다 컬렉션이 낫다

신명철·2022년 3월 14일
0

Effective Java

목록 보기
45/80

스트림과 반복

스트림은 반복을 지원하지 않는다. 스트림 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라 Iterable 인터페이스가 정의한 방식대로 동작하지만, 스트림이 Iterable을 extends 하지 않아서 반복할 수 없다.

for(ProcessHandle ph : ProcessHandle.allProcesses()::iterator){
	// 프로세스를 처리한다.
}
  • 위 코드는 컴파일되지 않는다.

위 코드와 같은 오류를 바로 잡고 스트림으로 반복을 사용하기 위해서는 어댑터를 사용해줘야 한다. 반대로 API가 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);
}

사용자와 반복문에서 사용하려는 사용자를 모두 배려해야 한다. 사용자가 한가지 방식만 사용할 거라는 근거가 없기 때문에 Stream과 Iterable의 공통 하위 인터페이스인 Collection 인터페이스를 사용해야 한다.

따라서, 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection 이나 그 하위 타입을 쓰는게 일반적으로 최선이다. 하지만 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올리면 안된다. 반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 컬렉션을 구현하는게 좋다.

다음 주어진 집합의 멱집합을 반환하는 전용 컬렉션 코드를 보자. (참고로 멱집합은 임의의 집합의 부분집합으로 이루어진 집합으로 원소의 개수는 2^n개가 되는 집합을 말한다.)

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 각각의 비트는 자연스럽게 멱집합의 원소 인덱스와 매핑된다. 이는 스트림으로도 작성할 수 있다. 위 코드보다 메모리 사용량은 많지만 간단히 작성할 수 있는 장점이 있다.

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()));
}

참고) 아래 코드는 위 코드의 취지와 비슷한 반복문이다.

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

원소 시퀀스를 반환할 수 있을 때는 스트림과 iterable을 모두 지원하는 컬렉션을 통해 반환하라. 원소의 개수가 많다면 멱집합의 예시처럼 전용 컬렉션을 만드는 편이 낫다.
컬렉션을 반환할 수 없다면 스트림과 Iterable 중 더 자연스러운 것을 반환하라.

profile
내 머릿속 지우개

0개의 댓글