이펙티브 자바, 쉽게 정리하기 - item 47. 반환 타입으로는 스트림보다 컬렉션이 낫다
Java8 이후 원소 시퀀스를 반환하는 방법
- 기존에는
Collection
,Iterable
이라는 선택지가 존재했다. - Java8 이후로는
Stream
이라는 선택지가 하나 더 늘었다. Stream
은 반환 타입으로 사용하기보다는 단순히 컬렉션 처리를 위해 사용하는 것이 좋다.- 반환은 다시 컬렉션으로 변경해주는 것이 활용성이 좋다.
someStream.collect(Collectors.toList())
와 같은 함수를 이용하면 쉽다.
Stream
이 Iterable
을 확장하지 않는데서 생기는 문제
- 기존에
Stream
의forEach()
는Consumer
인터페이스를 사용하는 만큼 값을 생산하기보다 소비하는데에 이용하는 것이 모범적이다.- 이 상황에서 일반 자바 API의
for each
문법을 사용하려 하면 다음과 같은 일이 벌어진다.
- 이 상황에서 일반 자바 API의
@Test
public void processHandleTest2() {
// 불편한 수동 캐스팅 필요
Iterable<ProcessHandle> processHandles = ProcessHandle.allProcesses()::iterator;
for (ProcessHandle processHandle : processHandles) {
System.out.println("processHandle = " + processHandle.info());
}
}
- 스트림 뒤에
::iterator
를 넣어도 IDE에서 기본으로 타입을 추론해주지 않는다.Runnable
,Executable
과 같은 타입을 추천해준다.
- 자바의 기본
for each
문법을 사용하려 하지 말고 스트림을 사용하면 깔끔하긴 하다.- 그런데 어떠한 이유로든
Iterable
을 이용해야 한다면, 수동 캐스팅을 해야 한다. ::iterator
보다는 애초에 반환 타입 자체를collect()
메서드를 통해 컬렉션으로 변경해주자.
- 그런데 어떠한 이유로든
Stream<ProcessHandle> processes = ProcessHandle.allProcesses();
processes.forEach(p -> System.out.println(p.info()));
어댑터 메서드 만들기
public static <E> Iterable<E> iterableOf(Stream<E> stream) {
return stream::iterator;
}
public static <E> Stream<E> streamOf(Iterable<E> iterable) {
return StreamSupport.stream(iterable.spliterator(), false);
}
iterableOf()
와streamOf()
라는 두가지 어댑터 메서드로 서로의 타입을 쉽게 오갈 수 있게 만들어 문제를 해결할 수도 있다.Collection
인터페이스는stream()
과Iterable
구현 모두 하기 때문에 기왕이면Collection
이나 그 하위 타입을 반환 혹은 파라미터 타입에 사용하는 게 최선이다.- 단지 컬렉션을 반환한다는 이유로 덩치 큰 시퀀스를 메모리에 올려서는 안된다.
- 시퀀스가 크지만, 표현 방식이 간단해질 수 있다면, 전용 컬렉션을 구현해보자.
어댑터 메서드를 통한 Stream
-> Iterable
-> Stream
변환 예제
@Test
void streamIterableTest() {
Stream<ProcessHandle> handleStream = ProcessHandle.allProcesses();
handleStream.forEach(p -> System.out.println(p.info()));
Iterable<ProcessHandle> handles = iterableOf(handleStream);
for (ProcessHandle handle : handles) {
System.out.println(handle.info());
}
Stream<ProcessHandle> stream = streamOf(handles);
stream.forEach(p -> System.out.println(p.info()));
}
Collection
타입이 나은 예: 멱집합을 위한 전용 컬렉션 만들기
멱집합이란? 모든 부분 집합을 원소로 가지는 집합이다.
static class PowerSet {
public static final <E> Collection<Set<E>> of(Set<E> s) {
List<E> src = new ArrayList<>(s);
// 30으로 제한하는 이유는 Integer.MAX_VALUE의 범위가 2^31 - 1이기 때문이다.
int numberOfMaximumElements = 30;
if(src.size() > numberOfMaximumElements){
throw new IllegalArgumentException("집합에 원소가 너무 많습니다. (최대 " + numberOfMaximumElements + " 개)");
}
return new AbstractList<>() {
@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;
}
@Override
public int size() {
// 멱집합의 크기는 2를 원래 집합 원소 수만큼 거듭제곱한 것과 같다.
return 1 << src.size();
}
@Override
public boolean contains(Object o) {
return o instanceof Set && src.containsAll((Set) o);
}
};
}
}
- 멱집합을 구해야 하는 경우엔 굳이 항상 모든 컬렉션 요소를 메모리상에 올리고 있을 필요는 없다.
get()
메서드를 통해 필요한 시점에 필요한 엘리먼트를 얻으면 된다.- 모든 요소를 가지고 있기엔
2^length
만큼의 공간을 확보해야 하는 부담이 있다. - 모든 요소를 가지고 있으면 매번 변경사항이 생길 때마다 멱집합을 새로 구해야 한다.
AbstractCollection
은contains()
와size()
만 구현해주면 구현 조건이 충족된다.- 이 경우가
Collection
을 반환하기 적당한 형태다.
Stream
타입이 나은 예: 부분 리스트를 스트림으로 변환하여 처리하기
static class SubLists {
public static <E> Stream<List<E>> of(List<E> list) {
Stream<List<E>> prefixes = prefixes(list);
System.out.println("prefixes = " + prefixes.toList());
Stream<List<E>> suffixes = suffixes(list);
System.out.println("suffixes = " + suffixes.toList());
return Stream.concat(prefixes(list), suffixes(list));
}
private static <E> Stream<List<E>> prefixes(List<E> list) {
return IntStream.rangeClosed(1, list.size())
.mapToObj(end -> list.subList(0, end));
}
private static <E> Stream<List<E>> suffixes(List<E> list) {
return IntStream.range(0, list.size())
.mapToObj(start -> list.subList(start, list.size()));
}
}
- 일단 스트림을 사용하면 반복이 더 자연스러운 상황에서도 스트림을 사용하기 쉽다.
- 이럴 때 어댑터를 이용할 수 있다.
- 하지만, 이는 클라이언트 코드를 어수선하게 만들며, 성능상 불이익을 가져온다.
- 전용 컬렉션을 만들면 코드는 조금 장황해지지만, 성능 자체는 어댑터보다 빠르다.
- 이럴 때 어댑터를 이용할 수 있다.
- 명확한 변환 과정과 평가시점이 명확할 때 스트림을 이용하자.
핵심 정리
- 스트림은 나름대로의 장단점이 있어서, 경우에 맞게 사용하는 것이 좋다.
- 가장 큰 장점이자 단점이 지연 평가가 된다는 것이다.
- 기본적으로는 컬렉션을 반환하는 게 유연하다.
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 49. 매개변수가 유효한지 검사하라 (0) | 2023.06.02 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 48. 스트림 병렬화는 주의해서 적용하라 (0) | 2023.05.31 |
이펙티브 자바, 쉽게 정리하기 - item 46. 스트림에서는 부작용 없는 함수를 사용하라 (0) | 2023.03.30 |
이펙티브 자바, 쉽게 정리하기 - item 44. 표준 함수형 인터페이스를 사용하라 (0) | 2023.03.29 |
이펙티브 자바, 쉽게 정리하기 - item 45. 스트림은 주의해서 사용하라 (0) | 2023.03.29 |