-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#47][B팀] 반환 타입으로는 스트림보다 컬렉션이 낫다
- Loading branch information
Showing
1 changed file
with
291 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,291 @@ | ||
# item 47. 반환 타입으로는 스트림보다 컬렉션이 낫다 | ||
|
||
전통적으로 일련의 시퀀스를 표현하는 방법으로 Java는 `Iterable`을 지원했다. 그리고 `List`, `Set`과 같은 컬렉션 인터페이스가 이를 지원한다. | ||
|
||
```java | ||
public interface Collection<E> extends Iterable<E> { | ||
|
||
// ... | ||
|
||
} | ||
``` | ||
|
||
Java 7 이하 버전에서는 일련의 시퀀스를 표현하는데 큰 어려움이 없었다. 단, Java 8에 `Stream`이 생기면서 상황이 달라졌다. | ||
|
||
## Iterable vs Stream | ||
|
||
`Stream`은 기본적으로 반복을 지원하지 않는다. | ||
|
||
```java | ||
// given | ||
List<Integer> numbers = Arrays.asList(1, 2, 3); | ||
|
||
// when | ||
for (int number : numbers.stream()) { | ||
// ... | ||
} | ||
``` | ||
|
||
위 코드에서 for-each문에 들어간 `numbers.stream()`는 컴파일에러가 난다. foreach loop는 `List`, `Set`과 같은 `Iterable`구현체에 대해서 사용이 가능하다. | ||
|
||
```java | ||
@Test | ||
void foreach_iterable() { | ||
// given | ||
List<Integer> numbers = Arrays.asList(1, 2, 3); | ||
|
||
// when & then | ||
for (int number: numbers) { | ||
assertThat(number <= 3).isEqualTo(true); | ||
} | ||
} | ||
``` | ||
|
||
위 코드는 아무 문제없이 동작한다. | ||
|
||
이런 차이로 만약 foreach loop를 사용하길 원하는 사용자에게 `Stream`을 반환하는 API를 제공한다면 불만을 토로할 것이다. 만약 이런 불만을 잠재우기 위해서 `Stream`을 `Iterable`로 변환하도록 지원하면 될까? | ||
|
||
### Stream을 Iterable로 변환해보기 | ||
|
||
사실 `Stream` 인터페이스는 `Iterable` 인터페이스가 정의한 추상 메서드를 모두 포함할 뿐만 아니라 `Iterable`이 정의한 방식대로 동작한다. | ||
|
||
```java | ||
package java.lang; | ||
|
||
import java.util.Iterator; | ||
import java.util.Objects; | ||
import java.util.Spliterator; | ||
import java.util.Spliterators; | ||
import java.util.function.Consumer; | ||
|
||
public interface Iterable<T> { | ||
|
||
Iterator<T> iterator(); | ||
|
||
default void forEach(Consumer<? super T> action) { | ||
Objects.requireNonNull(action); | ||
for (T t : this) { | ||
action.accept(t); | ||
} | ||
} | ||
|
||
default Spliterator<T> spliterator() { | ||
return Spliterators.spliteratorUnknownSize(iterator(), 0); | ||
} | ||
} | ||
``` | ||
|
||
```java | ||
public interface BaseStream<T, S extends BaseStream<T, S>> | ||
extends AutoCloseable { | ||
|
||
Iterator<T> iterator(); | ||
|
||
Spliterator<T> spliterator(); | ||
|
||
// ... | ||
} | ||
|
||
public interface Stream<T> extends BaseStream<T, Stream<T>> { | ||
|
||
// ... | ||
|
||
void forEach(Consumer<? super T> action); | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
위와 같이 `Iterable`에 존재하는 `iterable`, `forEach`, `spliterator`를 `Stream`에서도 지원한다. 이럼에도 `Stream`이 foreach에서 지원하지 않는 이유는 `Iterable`을 확장하지 않아서이다. | ||
|
||
```java | ||
@Test | ||
void foreach_stream() { | ||
// given | ||
Stream<Integer> numberStream = Stream.of(1, 2, 3); | ||
|
||
// when | ||
for (int number: numberStream::iterator) { | ||
assertThat(number <= 3).isEqualTo(true); | ||
} | ||
} | ||
``` | ||
|
||
위와 같이 `Stream#iteraor`를 메서드 참조로 전달하면 해결될 것으로 생각할 수 있다. | ||
|
||
![](https://user-images.githubusercontent.com/30178507/108869570-aac2b580-763a-11eb-91a5-fbd00211ca40.png) | ||
|
||
하지만 컴파일이 되지 않는다. 컴파일러가 타입 추론을 적절히 하지 못하기 때문이다. 이를 해결하려면 `Iterable`로 명시적 형변환이 필요하다. | ||
|
||
```java | ||
@Test | ||
void foreach_stream() { | ||
// given | ||
Stream<Integer> numberStream = Stream.of(1, 2, 3); | ||
|
||
// when | ||
for (int number: (Iterable<Integer>) numberStream::iterator) { | ||
assertThat(number <= 3).isEqualTo(true); | ||
} | ||
} | ||
``` | ||
|
||
`(Iterable<Integer>) numberStream::iterator` 이렇게 Iterable로 형변환함으로써 동작하도록 만들 수 있다. 다만, 위 코드는 직관성이 떨어지고 명시적 형변환을 필요로 한다. | ||
|
||
이를 보완하기 위해 어뎁터 메서드를 제공할 수 있다. | ||
|
||
```java | ||
@Test | ||
void foreach_stream() { | ||
// given | ||
Stream<Integer> numberStream = Stream.of(1, 2, 3); | ||
|
||
// when | ||
for (int number: getIterable(numberStream)) { | ||
assertThat(number <= 3).isEqualTo(true); | ||
} | ||
} | ||
|
||
private static <T> Iterable<T> getIterable(Stream<T> stream) { | ||
return stream::iterator; | ||
} | ||
``` | ||
|
||
**참고. `stream::iterator`가 Iterable이 되는 이유** | ||
|
||
위 Iterable 인터페이스를 보면 `iterator`를 제외한 `forEach`와 `spliterator`는 이미 구현이 되어있다. | ||
|
||
```java | ||
public interface Iterable<T> { | ||
Iterator<T> iterator(); | ||
|
||
default void forEach(Consumer<? super T> action) { | ||
Objects.requireNonNull(action); | ||
for (T t : this) { | ||
action.accept(t); | ||
} | ||
} | ||
|
||
default Spliterator<T> spliterator() { | ||
return Spliterators.spliteratorUnknownSize(iterator(), 0); | ||
} | ||
} | ||
``` | ||
|
||
즉, Iterable은 `iterator`를 람다로 제공할 수 있다는 뜻이 된다. | ||
|
||
```java | ||
@Test | ||
void foreach_stream() { | ||
// given | ||
Stream<Integer> numberStream = Stream.of(1, 2, 3); | ||
|
||
// when | ||
for (int number: (Iterable<Integer>) () -> numberStream.iterator()) { | ||
assertThat(number <= 3).isEqualTo(true); | ||
} | ||
} | ||
``` | ||
|
||
따라서 메서드 레퍼런스를 처음 `Iterable`로 형변환한 코드를 위와 같이 람다로 변환할 수 있다. | ||
그리고 위 람다가 `Iterable#iterator`를 구현한 것이 되므로 메서드 레퍼런스로 변환이 가능한 것이다. | ||
|
||
### Iterable을 Stream으로 변환해보기 | ||
|
||
반대로 `Iterable`을 반환하는 API만 있다면 `Stream`을 사용하고 싶은 사용자 입장에서는 불만이 있을 것이다. 이를 위한 어뎁터 메서드도 쉽게 구현이 가능하다. | ||
|
||
```java | ||
private static <T> Stream<T> getStream(Iterable<T> iterable) { | ||
return StreamSupport.stream(iterable.spliterator(), false); | ||
} | ||
``` | ||
|
||
Java 표준라이브러리에서 제공하는 `StreamSupport#stream`을 통해 어뎁터 메서드 구현이 가능하다. | ||
|
||
```java | ||
@Test | ||
void foreach_stream_adaptor() { | ||
// given | ||
List<Integer> numbers = Arrays.asList(1, 2, 3); | ||
|
||
// when | ||
Stream<Integer> numberStream = getStream(numbers); | ||
|
||
// then | ||
numberStream.forEach(number -> assertThat(number <= 3).isTrue()); | ||
} | ||
``` | ||
|
||
## Iterable과 Stream 뭘 반환해야해? | ||
|
||
어뎁터 메서드는 클라이언트 코드를 어수선하게 만들고 2, 3배 가량 성능이 좋지 못하다. | ||
|
||
만약 객체 시퀀스를 반환하는데 해당 메서드가 무조건 `Stream`으로만 사용이 된다면 `Stream`을 반환해도 무방하다. 반대로 무조건 `Iterable`로만, 즉, 반복문에서만 사용된다면 `Iterable`로 반환해도 무방하다. | ||
|
||
하지만 공개 API를 작성할 때는 `Stream`을 원하는 사용자와 `Iterable`을 원하는 사용자 모두를 배려해야한다. | ||
|
||
여기서 `List`, `Set` 등의 상위 타입인 `Collection`을 보면 `Iterable`의 하위 타입이면서 `Stream`을 반환하는 `stream` 메서드도 제공한다. | ||
|
||
```java | ||
public interface Collection<E> extends Iterable<E> { | ||
// ... | ||
|
||
default Stream<E> stream() { | ||
return StreamSupport.stream(spliterator(), false); | ||
} | ||
|
||
// ... | ||
} | ||
``` | ||
|
||
때문에 공개 API의 반환 타입으로는 `Collection`의 하위 타입을 사용한다면 `Iterable`과 `Stream`을 원하는 사용자를 만족시킬 수 있다. | ||
|
||
> 배열도 Java 표준 API인 `Arrays#asList`와 `Stream#of`로 Iterable과 Stream을 동시에 지원할 수 있다. | ||
## 반환하는 컬렉션이 너무 크다면 전용 컬렉션도 고려하라 | ||
|
||
다만 컬렉션을 반환한다는 이유로 크기가 매우 큰 컬렉션을 메모리에 올리는 것은 좋지 못한 생각이다. 이 경우는 전용 컬렉션을 구현하는 방법도 있다. | ||
|
||
```java | ||
import java.util.*; | ||
|
||
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; | ||
} | ||
}; | ||
} | ||
} | ||
``` | ||
|
||
위 예시는 멱집합을 전용 컬렉션으로 직접 구현한 코드이다. 멱집합이란 한 집합의 모든 부분집합을 의미한다. 즉, 원소 갯수가 `n`이라면 $2^n$이 멱집합의 원소 갯수이다. | ||
|
||
이를 메모리에 올린다고 한다면 너무나도 많은 메모리가 소모될 것이다. 때문에 위와 같은 멱집합을 구현하는 전용 컬렉션을 직접 구현하여 사용할 수 있다. 위 예시의 `PowerSet`은 각 원소의 인덱스를 비트 백터로 사용하므로 메모리 공간을 효율적으로 사용할 수 있다. | ||
|
||
참고로 `AbstractCollection`을 활용할 때는 `Iterable`용 메서드 외에 `contains`와 `size`만 구현하면 된다. | ||
|
||
### 정리 | ||
|
||
1. `Iterable`과 `Stream`은 어뎁터 메서드를 통해서 서로 변환이 가능하다. | ||
2. 단, 어뎁터 메서드는 클라이언트 메서드를 어수선하게 만들며 성능 또한 좋지 못하다. | ||
3. 만약 반환값이 `Stream`만 쓴다고 보장된다면 `Stream`을 반환해도 무방하다. 반대로 `Iterable`의 경우도 마찬가지이다. | ||
4. 사용자를 생각하여 반환값은 가능하면 `Iterable`의 하위타입이면서 `stream`을 지원하는 `Collection`의 하위타입을 반환하자. |