Skip to content

Commit

Permalink
[#47][B팀] 반환 타입으로는 스트림보다 컬렉션이 낫다
Browse files Browse the repository at this point in the history
[#47][B팀] 반환 타입으로는 스트림보다 컬렉션이 낫다
  • Loading branch information
ksy90101 authored Feb 28, 2021
2 parents 032846f + ed4775a commit 368d31f
Showing 1 changed file with 291 additions and 0 deletions.
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`의 하위타입을 반환하자.

0 comments on commit 368d31f

Please sign in to comment.