DEV ℧ Developer Diary

[EffectiveJava] item47 - 반환 타입으로는 스트림보다 컬렉션이 낫다.

일련의 원소를 반환하는 메서드는 수없이 많다. 자바 7까지는 이런 메서드의 반환타입으로 Collection, Set, List 같은 컬렉션 인터페이스, 혹은 Iterable이나 배열을 썼다.

그런데 자바 8이 스트림이라는 개념을 들고 오면서 이 선택이 아주 복잡한 일이 되어버렸다.

Stream과 Iterable의 차이점

스트림은 반복(iteration)을 지원하지 않는다. 따라서 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.

Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작하지만 Stream은 Iterable을 확장하지 않았기 때문에, for-each 스트림을 반복 할 수 없다.

Stream<E>의 Iterable<E>우회법

자바 타입 추론의 한계로 컴파일 되지 않는다.

for (ProcessHandle pg : ProcessHandle.allProcesses()::iterator) {
    /** 프로세스를 처리한다 */
}

Stream의 iterator 메서드에 메서드 참조를 건네면 해결될것 같지만, 위의 코드는 컴파일 오류가 발생한다.

스트림을 반복하기 위한 ‘끔찍한’ 우회 방법

for(processHandle ph : (Iterable<ProcessHandle>)
                        ProcessHandle.allProcesses()::iterator){
    /** 프로세스를 처리한다. */
}

작동은 하지만 실전에 쓰기에는 너무 난잡하고 직관성이 떨어진다.

Stream<E>를 Iterable<E>로 중개해주는 어댑터

public static <E> Iterable<E> iterableOf(Stream<E> stream) {
    return stream::iterator;
}

위와 같은 어댑터 메서드를 사용하면 어떤 스트림도 for-each 문으로 반복할 수 있어서 상황이 나아진다.

for(ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
    /** 프로세스를 처리한다. */
}

Iterable<E>의 Stream<E>우회법

반대로 API가 Iterable만 반환하면 이를 스트림 파이프 라인에서 처리하기 힘들것이다. 이를 위한 어댑터도 손쉽게 구현이 가능하다.

Iterable<E>를 Stream<E>로 중개해주는 어댑터

public Static <E> Stream<E> streamOf(Iterable<E> iterable) {
    return StreamSupport.stream(iterable.spliterator(), false);
}

객체 시퀀스를 반환하는 메서드를 작성하는데, 이 메서드가 오직 스트림 파이프라인에서만 쓰일 걸 안다면 마음 놓고 스트림을 반환하게 해주자. 반대로 반환된 객체들이 반복문에서만 쓰일 걸 안다면 Iterable을 반환 하자.

공개 API의 반환 값

Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다.

원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선이다.

반환하는 시퀀스의 크기가 메모리에 올려도 안전할 만큼 작다면 ArrayListHashSet 같은 표준 컬렉션 구현체를 반환하는게 최선일 수 있다. 하지만 단지 컬렉션을 반환한 다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안된다.

AbstractCollection과 같은 Collection 구현체를 작성할 때는 containssize를 재정의 해줘야 한다.

재정의가 불가능한 상황일때는 Stream이나 Iterable을 반환하는 편이 낫다.

Collection을 반환타입으로 쓸때의 단점은, Collection의 size 메서드가 int 값을 반환하므로 시퀀스의 최대 길이는 Interger.MAX_VALUE 혹은 2³¹ - 1로 제한된다.
Collection 명세에 따르면 컬렉션이 더 크거나 심지어 무한대일 때 size가 2³¹ - 1을 반환해도 되지만 완전히 만족스러운 해법은 아니다.

Stream<E> 반환

입력 리스트의 모든 부분리스트를 스트림으로 구현하기는 어렵지 않다.

두가지 방법이 있다.

입력 리스트의 모든 부분리스트를 스트림으로 반환한다.

public class SubLists {
    public static <E> Stream<List<E>> of(List<E> list) {
        return Stream.concat(Stream.of(Collections.emptyList()),
          prefixes(list).flatMap(SubLists::suffxes));
    }

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

입력 리스트의 모든 부분리스트를 스트림으로 반환한다.

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

이상으로 스트림을 반환하는 두가지 구현을 알아봤는데 모두 쓸만은 하지만 반복을 사용하는 게 더 자연스러운 상황에서도 사용자는 그냥 스트림을 쓰거나 Stream을 Iterable로 변환해주는 어댑터를 이용해야 한다.

하지만 이러한 어댑터는 클라이언트 코드를 어수선하게 만들고 2.3배가 더 느리게 되었다.

하지만 직접 구현한 전용 Collections을 사용하니 코드는 훨씬 지저분했지만, 스트림을 활용한 구현보다 1.4배 빨랐다고 한다.

정리

컬렉션을 반환할수 있다면 그렇게 하자. 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 적다면 ArrayList같은 표준 컬렉션에 담아 반환하자.

컬렉션을 반환하는게 불가능 하면 스트림과 Iterable 중 더 자연스러운 것을 반환하자.