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

Loopy·2022년 9월 24일
0

이펙티브 자바

목록 보기
46/76
post-thumbnail

1) 스트림 도입 이전

스트림이 도입되기 이전에 원소 시퀀스, 즉 일련의 원소를 반환하는 메서드의 반환 타입으로 Collection, Set, List과 같은 컬렉션 인터페이스나 Iterable 또는 배열을 사용했다.

2) 스트림 도입 이후

스트림은 반복(iteration)을 지원하지 않는다. 따라서 원소 시퀀스를 반환할때 스트림을 사용하면 아래와 같이 for-each 로 반복을 수행할 수 없다.

static Stream<ProcessHandle> allProcesses()

// Stream.iterator 사용
for(ProcessHandle p: ProcessHandle.allProcesses.iterator()) {

}

for-each 와 같이 향상된 for 문이 가능한 컬렉션은 Iterable 인터페이스를 구현하고 있어야 하기 때문이다. Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 포함하고 정의한 방식대로 동작하지만, 확장(extend) 하지 않았기 때문에 반복이 불가능하다.

public interface Stream<T> extends BaseStream<T, Stream<T>> {
} 

그렇다면 스트림을 반복할 수 있게 하려면 어떻게 해야 할까?

☁️ 어댑터(Adapter)

1) Stream -> Iterable

Stream<E>Iterable<E> 로 중개해주는 어댑터이다.

public static <E> Iterable<E> iterableOf(Stream<E> stream) { 
        return stream::iterator;
}  
for(ProcessHandle p : iterableOf(ProcessHandle.allProcesses())){
}

어댑터를 사용한다면, 어떤 스트림도 for-each 문으로 반복할 수 있다. 참고로 자바의 타입 추론이 문맥을 잘 파악하기 때문에 어댑터 메서드 안에서 따로 형변환 하지 않아도 된다.

2) Iterable -> Stream

반대로 Iterable<E>Stream<E> 로 변환해주는 어댑터도 다음과 같이 쉽게 구현 가능하다.

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

☁️ Collection 인터페이스

객체 시퀀스를 반환하는 메서드를 할 때, 스트림 파이프라인과 반복문 둘 중에 한 방식만 사용할 거라는 근거가 없으므로 어댑터를 쓰기 애매하다.

하지만 Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다. 따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection 이나 그 하위 타입을 쓰도록 하자.

public interface Collection<E> extends Iterable<E> {
    ...
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    
    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

전용 컬렉션 구현 예제

  1. 원소 사이즈가 작은 경우 : 표준 컬렉션
  2. 원소 사이즈가 큰 경우 : 전용 컬렉션 구현

반환할 시퀀스가 크지만 표현을 간결하게 할 수 있다면, 전용 컬렉션을 구현할 수 있다.

다음의 예제를 보면, 멱집합의 경우 원소의 개수가 n개일때 멱집합의 원소 개수는 2^n 개가 되므로, 표준 컬렉션 구현체에 저장하면 위험하다. 따라서 아래와 같이 AbstractList 를 이용해서 각 원소의 인덱스를 비트 벡터로 사용하는 방식으로 바꿀 수 있다. (비트 마스킹)

🔖 {a, b, c} 의 멱집합 표현하기

000 : {}
001 : {a}
...
111 : {a, b, c}
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);
            }

			// 인덱스 n 번째 비트 값 : 해당 원소가 원래 집합의 n 번째 원소를 포함하는지 여부
            @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;
            }
        };
    }
}

스트림 반환하는 예제

입력 리스트의 모든 부분 리스트 반환해야 하는 상황이다.

일반적으로 O(N^2) 성능이 나오는 이중 반복문 방식을 사용할 수 있지만, 이렇게 되면 해당 컬렉션은 입력 리스트 크기의 거듭제곱만큼 메모리를 차지하게 된다.

for (int start = 0; start < src.size(); start++) {
	for (int end = start + 1; end <= src.size(); end++) {
    	System.out.println(src.subList(start, end));
    }
}

컬렉션 대신 스트림을 반환하면, 다음과 같다.
모든 부분 리스트를 prefix + suffix + {} 로 분리해서 볼 수 있는데, 이는 스트림을 통해 가독성 좋게 구현할 수 있다.

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) { // (a), (a,b), (a,b,c)
        return IntStream.rangeClosed(1, list.size())
                .mapToObj(end -> list.subList(0, end));
    }

    private static <E> Stream<List<E>> suffixes(List<E> list) { // (a,b,c), (b,c), (c)
        return IntStream.range(0, list.size())
                .mapToObj(start -> list.subList(start, list.size()));
    }
}
  • Stream.concat() : 두 개의 Stream 을 전달받아 하나로 합쳐진 새로운 Stream 을 반환
  • Stream.flatMap() : 중복된 스트림을 1차원으로 평면화

📚 핵심 정리
반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 원소 개수가 적다면 ArrayList 과 같은 표준 컬렉션에 담아 반환하자. 그렇지 않다면, 전용 컬렉션을 구현할 수도 있다. 컬렉션을 반환하는게 불가능하다면 스트림과 Iterable 중 더 자연스러운 것을 반환하면 된다.

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글