[Java] Item 47 : 반환 타입으로는 스트림보다 컬렉션이 낫다
Effective Java를 읽고 정리한 정리본입니다.
📌 Item 47 : 반환 타입으로는 스트림보다 컬렉션이 낫다
자바 7까지는 원소 시퀀스 (일련의 원소) 메서드 반환 타입으로 Collection, Set, List 같은 컬렉션 인터페이스, 혹은 Iterable 배열을 사용했다.
기본은 컬렉션 인터페이스이며, 다음과 같은 특수한 상황에서는 Iterable 인터페이스를 사용했다.
- for-each문
- 일부 Collection 메서드를 구현할 수 없는 경우
-> ex) contains(Object) -> boolean 값 반환함.
또는 반환 원소들이 기본 타입이거나 성능에 민감한 상황이라면 배열을 사용했다.
그러나 자바 8에 스트림이 추가되며 선택지에 스트림이 추가되었다.
🫧 Stream과 Iterable
아이템 45에서 이야기했듯, 스트림은 반복(iteration)을 지원하지 않는다.
하지만, 사실 Stream 인터페이스는 Iterable 인터페이스가 정의한 추상 메서드를 전부 포함할 뿐만 아니라, Iterable 인터페이스가 정의한 방식대로 동작한다.
그럼에도 for-each로 스트림을 반복할 수 없는 까닭은 바로 Stream이 iterable을 확장하지 않았기 때문이다.
확장하지 않으면 타입 호환성이 맞지 않기 때문이다.
안타깝게도 이 문제를 해결해줄 멋진 우회로는 없다.
얼핏 보면 Stream의 iterator 메서드에 메서드 참조를 건네면 해결될 것 같지만, 아쉽게도 컴파일 오류가 난다.
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator) {
// 프로세스 처리 코드
}
이 코드는 다음과 같은 오류를 낸다. 자바가 타입을 추론할 수 없어 컴파일이 불가한 것이다.
Test.java:6: error: method reference not expected here
for (ProcessHandle ph : ProcessHandle.allProcesses()::iterator)
실제로 ProcessHandle.allProcesses는 Stream
그렇다면 형변환을 하면 이 문제가 해결될까?
for (ProcessHandle ph : (Iterable<ProcessHandle>) ProcessHandle.allProcesses()::iterator) {
// 프로세스 처리 코드
}
작동은 하지만, 너무 난잡하고 직관성이 떨어지는 코드이다.
이를 해결하기 위해서 어댑터 메서드를 활용할 수 있는데, 이때 자바 타입 추론이 문맥을 잘 파악해 굳이 형변환할 필요가 없다.
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
다음은 사용 예시이다. 어댑터 사용 시 어떤 스트림도 for-each문으로 반복 가능하다.
for (ProcessHandle p : iterableOf(ProcessHandle.allProcesses)) {
// 프로세스 처리 코드
}
반대로, Iterator을 Stream으로 바꿔주는 어댑터도 손쉽게 구현이 가능하다.
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
🫧 스트림 대신 Collection을 반환하자
Collection 인터페이스는 Iterable의 하위 타입이고 stream 메서드도 제공하니 반복과 스트림을 동시에 지원한다.
따라서 원소 시퀀스를 반환하는 공개 API의 반환 타입에는 Collection이나 그 하위 타입을 쓰는 게 일반적으로 최선이다.
그러나 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안 된다.
🫧 시퀀스 크기가 크다면 전용 컬렉션 구현을 고려하자
예컨데 주어진 집합의 멱집합(한 집합의 모든 부분집합을 원소로 하는 집합)을 반환하는 상황이다.
{a, b, c}의 멱집합은 { {}, {a}, {b}, {c}, {a, b}, {b, c}, {a, c}, {a, b, c}}이다.
원소 개수가 n개면 멱집합의 원소 개수는 2^n개가 된다.
기하급수적으로 늘어나는 숫자로, 이러한 멱집합을 표준 컬렉션 구현체에 저장한다면 크기가 너무 커 질 것이다.
따라서, AbstractList를 이용해 전용 컬렉션을 구현하는 방식을 사용하자.
✨ 전용 클래스 만들기 예 - 멱집합
아이디어는 멱집합을 구성하는 각 원소의 인덱스를 비트 벡터로 사용하는 것이다.
인덱스의 n번째 비트 값은 멱집합의 해당 원소가 원래 집합의 n번째 원소를 포함하는지 여부를 나타낸다.
에를 들어 {a, b, c}의 멱집합은 원소가 8개이므로 유효한 인덱스는 0-7이며, 이진수로는 000~111이다.
이때 인덱스를 이진수로 나타내면 각 n번째 자리의 값이 각각 원소 a, b, c를 포함하는지 알려줄 수 있다.
즉, 멱집합의 000번째 원소는 {}, 101번째 원소는 {a, 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();
}
@Overrie 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용 메서드 외에 2개만 더 구현하면 된다. (contains, size)
만일 반복이 시작되기 전 시퀀스의 내용을 확정할 수 없는 등의 사유로 contains와 size를 구현하는 것이 불가능한 경우 컬렉션보다는 스트림이나 Iterable을 반환하는 편이 낫다.