Home [Effective Java] Item47. 반환 타입으로는 스트림보다 컬렉션이 낫다!
Post
Cancel

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

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

Intro

  • 원소 시퀀스, 즉 일련의 원소를 반환하는 메서드는 수없이 많다.
  • 자바 7까지는 이런 메서드의 반환 타입으로 Collection, Set, List 같은 컬렉션 인터페이스 또는 Iterable이나 배열을 사용했다.
    • 기본은 컬렉션 인터페이스를 사용한다.
    • 반환된 원소 시퀀스가 일부 Collection 메서드를 구현할 수 없을 때 Iterable 인터페이스를 사용.
    • 반환된 원소들이 기본 타입이거나 성능에 민감한 상황일 때 배열을 사용.
  • 자바 8에서 스트림이 추가되면서 선택지가 늘어나게 되었다.

스트림과 반복

  • 스트림은 반복(iteration)을 지원하지 않는다. 따라서 스트림과 반복을 알맞게 조합해야 한다.
  • 사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함하고 있고, Iterable 인터페이스가 정의한 방식대로 동작한다.
    • 그럼에도 for-each 반복을 못하는 이유는 Stream이 Iterable을 확장(extend)하지 않아서다.

Stream의 iterator 메서드에 메서드 참조를 건넨다.

1
2
3
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
  	System.out.println(ph);
}
  • 형변환이 제대로 되지 않아 오류가 발생한다.
1
2
3
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator) {
  	System.out.println(ph);
}
  • 캐스팅을 통해 형변환을 시켜주는 ‘끔찍한’ 우회 방법이다.
    • 작동은 하지만 실전에 쓰기에는 너무 난잡하고 직관성이 떨어진다.
    • 어댑터 메서드를 통해 개선할 수 있다.

타입을 중개해주는 어댑터 메서드

  • 어댑터를 사용하면 어떤 스트림도 for-each 문으로 반복할 수 있다.
1
2
3
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
		return stream::iterator;
}
1
2
3
for (ProcessHandle ph : iterableOf(ProcessHandle.allProcesses())) {
  	System.out.println(ph);
}
  • Iterable을 Stream으로 중개해주는 어댑터도 있다.
1
2
3
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
		return StreamSupport.stream(iterable.spliterator(), false);
}

반환할 타입

  • 객체 시퀀스를 반환하는 메서드를 작성하는데, 이 메서드가 오직 스트림 파이프라인에서만 쓰일 것을 안다면 스트림을 반환하게 하자.
    • 반대로 반환된 객체들이 반복문에서만 쓰일 것을 안다면 Iterable을 반환하자.
  • 공개 API 작성 시 스트림, 반복문 사용자를 모두 고려해 주어야 한다.
    • 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
27
28
29
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);
            }
            @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;
            }
        };
    }
}
  • 주어진 집합의 멱집합을 구하는 예제다.

    멱집합 : 한 집합의 모든 부분집합을 원소로 하는 집합

  • AbstractList를 이용해 훌륭한 전용 컬렉션을 구성할 수 있다.

  • 입력 집합의 원소 수가 30을 넘어가면 PowerSet.of가 예외를 던진다.

    • 반환되는 시퀀스의 최대 길이는 Integer.MAX_VALUE 값인 2^32-1로 제한되기 때문이다.
  • 멱집합을 구성하는 각 원소의 인덱스를 비트 벡터로 사용해 메모리에 거대한 컬렉션을 올리지 않고 사용이 가능하다.

Collection 구현체 작성

  • Iterable 용 메서드 외에 2개만 더 구현하면 된다.
    • contains
    • size
  • (반복이 시작되기 전에는 시퀀스의 내용을 확정할 수 없는 등의 사유로) contains와 size를 구현하는 것이 불가능할 때는 컬렉션보다는 스트림이나 Iterable을 반환하는 편이 더 낫다.

핵심 정리

  • 원소 시퀀스를 반한하는 메서드를 작성할 때는, 이를 스트림으로 처리하기를 원사는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있을 수 있음을 떠올려 양쪽을 다 만족시키려 노력해야 한다.
  • 컬렉션을 반환할 수 있다면 그렇게 하자.
    • 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나, 컬렉션을 하나 더 만들어도 될 정도로 원소 개수가 적다면 ArrayList 같은 표준 컬렉션에 담아 반환하라.
    • 그렇지 않다면, 앞서의 멱집합 예처럼 전용 컬렉션을 구현할지 고민하라.
  • 컬렉션을 반환하는 게 불가능하면 스트림과 Iterable 중 더 자연스러운 것을 반환하라.
    • 향후 Stream 인터페이스가 Iterable을 지원하도록 자바가 수정된다면, 그때는 안심하고 스트림 반환을 할 수 있을 것이다.
This post is licensed under younghwani by the author.

[Effective Java] Item46. 스트림에서는 부작용 없는 함수를 사용하라!

[Effective Java] Item48. 스트림 병렬화는 주의해서 적용하라!

Comments powered by Disqus.