Skip to content

item 47 JihoonKim

hoonti06 edited this page Aug 21, 2020 · 4 revisions

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

일련의 원소(원소 시퀀스)를 리턴할 때의 타입

  • Collection, Set, List 같은 컬렉션 인터페이스(기본)
  • Iterable
  • 배열(성능에 민감한 상황)
  • 스트림

스트림이 도입되면서 리턴 타입의 선택이 복잡해졌다.

스트림의 반복 지원

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

// Stream의 iterator 메서드에 메서드 참조를 건네 스트림의 반복을 지원 - Compile 에러(타입 추론 한계)
for (ProcessHandle p : ProcessHandle.allProcesses()::iterator) { 
	// process 처리
}

// Stream의 iterator 메서드에 메서드 참조를 건네어 스트림의 반복을 지원 - 스트림 반복을 위한 '끔찍한' 우회 방법
for (ProcessHandle p : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator) {
	// process 처리
}

어댑터 메서드 작성을 통해 개선 가능하다.

// Stream<E>를 Iterable<E>로 중개해주는 어댑터
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
	return stream::iterator;
}

for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses())) {
	// process 처리
}
// Iterable<E>를 Stream<E>로 중개해주는 어댑터
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
	return StreamSupport.stream(iterable.spliterator(), false); // 2nd param은 parallel 여부
}

공개 API를 작성한다면 스트림 파이프라인 사용자와 반복문 사용자 모두를 배려해야 한다.

일반적으로 Collection 또는 그 하위 타입을 쓰는 게 최선이다.

Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다.
따라서, 원소 시퀀스를 리턴하는 공개 API의 리턴 타입에는 일반적으로 Collection 또는 그 하위 타입을 쓰는 게 최선이다.
하지만, 단지 컬렉션을 리턴한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.

리턴할 시퀀스가 크지만 표현을 간결하게 할 수 있다면 전용 Collection을 구현하는 방안도 생각해보자.

전용 Collection 예제

주어진 집합의 멱집합(한 집합의 모든 부분집합을 원소로 하는 집합)을 리턴하는 상황
원소 개수가 n개 이면 멱집합의 원소 개수는 2^n개(지수배로 증가)

참고) {a, b, c}의 멱집합은 { {}, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c} }

AbstractList를 이용하여 컬렉션 구현

  • 각 원소의 인덱스를 비트벡터로 사용(n번째 비트 값은 해당 원소가 n번째 원소 포함 여부)
  • 0부터 2^n - 1 까지의 이진수와의 매핑
public class PowerSet {
    public static final <E> Collection<Set<E>> of(Set<E> s) {
       List<E> src = new ArrayList<>(s);
       if(src.size() > 30) { // Collection을 리턴 타입으로 쓸 때의 단점
           throw new IllegalArgumentException("집합에 원소가 너무 많습니다(최대 30개).: " + s);
       }

       return new AbstractList<Set<E>>() {
           @Override
           public int size() {
               return 1 << src.size(); // 2^n
           }

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

AbstractCollection을 활용하여 Collection 구현체를 작성할 때는 Iterable용 메서드 외의 contains와 size만 더 구현하면 된다.
contains와 size를 구현하는 게 불가능하다면 컬렉션보다는 스트림이나 Iterable을 리턴하는 편이 낫다(별도의 메서드를 부어 두 방식 모두 제공해도 된다).

입력 리스트의 모든 부분리스트를 스트림으로 리턴 예제

부분 리스트를 구하는 방법

  • (a, b, c)의 prefixes - (a), (a, b), (a, b, c)
  • (a, b, c)의 suffixes - (c), (b, c), (a, b, c)
  • 빈 리스트(empty list)
public class SubList {
    public static <E> Stream<List<E>> of(List<E> list) {
        return Stream.concat(Stream.of(Collections.emptyList()),			// 빈 리스트
                             prefixes(list).flatMap(SubList::suffixes));
    }

    public static <E> Stream<List<E>> prefixes(List<E> list) {
        return IntStream.rangeClosed(1, list.size())
                        .mapToObj(end -> list.subList(0, end));
    }

    public static <E> Stream<List<E>> suffixes(List<E> list) {
        return IntStream.rangeClosed(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);
}

스트림 리턴만 제공되니 반복문 사용이 필요할 때에는 Iterable로 변환해주는 어댑터를 이용해야 한다. (코드가 어수선해지고, 작가 컴퓨터로 2.3배 더 느리다.) (책에는 안 나온) 직접 구현한 Collection이 스트림보다 1.4배(작가 컴퓨터) 빨랐다(코드는 더 지저분해졌다).

핵심 정리

  • 원소 시퀀스를 리턴하는 메서드를 작성할 때 스트림 사용자와 반복문 사용자가 모두 있을 수 있기에 양쪽을 만족시키려고 노력하자
  • 컬렉션을 리턴할 수 있다면 컬렉션을 이용하자
    • 원소 개수가 적거나 이미 컬렉션에 담아 관리하고 있으면 표준 컬렉션(E.g ArrayList)을 이용하자
    • 그 외에는 전용 컬렉션(E.g 멱집합)을 구현하는 것을 고려하자
  • 컬렉션을 리턴할 수 없다면 스트림이나 Iterable 중 더 나은 것을 이용하자
  • 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 수정된다면, 스트림 처리와 반복에 모두 사용 가능하니 안심하고 스트림을 리턴하면 될 것이다.
Clone this wiki locally