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

wisdom·2022년 9월 23일
0

Effetctive Java

목록 보기
47/80
post-thumbnail

1. 원소 시퀀스 반환 타입

  1. 컬랙션 인터페이스 (Collection, Set, List)
  2. Iterable 인터페이스
  3. 배열
  4. 스트림 (자바 8부터 지원)

자바 7까지의 원소 시퀀스 반환 타입의 선택

  • 기본적으로 컬렉션 인터페이스를 사용
  • for-each문에서만 사용하거나, 원소 시퀀스가 일부 Collection 메서드를 구현할 수 없을 때 Iterable 인터페이스를 사용
  • 반환 원소들이 기본 타입이거나 성능에 민감한 상황이라면 배열을 사용

그러나 자바 8부터 스트림을 지원하면서 반환 타입을 선택하는 게 복잡한 일이 되었다.


2. Stream vs. Iterable

스트림은 반복(iteration)을 지원하지 않는다.

Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작한다.

그렇지만 Stream은 Iterable을 extends 하지 않았기 때문에 for-each로 스트림을 반복할 수 없다.

따라서 스트림과 반복을 알맞게 조합해야 좋은 코드가 나온다.

1) Stream을 반복하기 위한 방법

스트림을 반복하기 위한 방법은 알아보자.

다음과 같은 어댑터를 사용하면 어떤 스트림도 for-each 문으로 반복할 수 있다.

// Stream<E>를 Iterable<E>로 중개해주는 어댑터
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
	return stream::iterator;
}
// 사용 예제
for (ProcessHandle p : iterableOf(ProcessHandle.allProcess())) {
	// 프로세스 처리
}

2) Iterable을 스트림 파이프라인에서 처리하기 위한 방법

이번엔 반대로 Iterable을 스트림 파이프라인에서 사용할 수 있도록 하는 방법을 알아보자.

다음과 같은 어댑터를 이용하면 된다.

// Iterable<E>를 Stream<E>로 중개해주는 어댑터
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
    return StreamSupport.stream(iterable.spliterator(), false);
}

3) 정리

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

하지만 공개 API를 작성할 때는 스트림 파이프라인을 사용하는 사람과 반복문에서 쓰려는 사람 모두를 배려해야 한다. → 컬렉션 인터페이스!


3. Collection

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

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

Arrays 역시 Arrays.asList 와 Stream.of 메서드로 손쉽게 반복과 스트림을 반환할 수 있다.

반환하는 시퀀스의 크기가 메모리에 올려도 안전할 만큼 작다면 ArrayList나 HashSet 같은 표준 컬렉션 구현체를 반환하는 것이 최선이다.

만약 반환할 시퀀스가 크다면 전용 컬렉션을 구현하는 방안을 검토하자.

전용 컬렉션

AbstractList 를 이용하면 전용 컬렉션을 손쉽게 구현할 수 있다.

멱집합(한 집합의 모든 부분집합을 원소로 하는 집합)을 반환하는 예제를 보자.

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

멱집합을 구성하는 각 원소의 인덱스를 비트 벡터로 사용하였다.

+) AbstractCollection 을 활용하는 방법

AbstractCollection 을 활용해서 Collection 구현체를 작성할 때는 Iterable 용 메서드 외에 2개만 더 구현하면 된다. 바로 contains와 size다.
contains 와 size를 구현하는 게 불가능할 때는 컬렉션보다는 스트림이나 Iterable을 반환하는 편이 낫다.


📌 핵심 정리

  • 원소 시퀀스를 반환하는 메서드를 작성할 때는, 이를 스트림으로 처리하길 원하는 사용자와 반복으로 처리하길 원하는 사용자가 모두 있을 수 있음을 떠올리고, 양쪽을 다 만족시키려 노력하자.
  • 컬렉션을 반환할 수 있다면 그렇게 하라.
    반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나, 컬렉션을 하나 만들어도 될 정도로 원소 개수가 적다면 ArrayList 같은 표준 컬렉션에 담아 반환하라.
    그렇지 않으면 전용 컬렉션을 구현할지 고민하라.
  • 컬렉션을 반환하는 게 불가능하면 스트림Iterable 중 더 자연스러운 것을 반환하라.
  • 만약 나중에 Stream 인터페이스가 Iterable을 지원하도록 자바가 수정된다면, 그때는 안심하고 스트림을 반환하면 될 것이다. (스트림 처리와 반복 모두에 사용할 수 있기 때문)
profile
백엔드 개발자

0개의 댓글