-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 아이템 46 * 아이템 47
- Loading branch information
Showing
3 changed files
with
300 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,162 @@ | ||
# [item 46] 스트림에서는 부작용 없는 함수를 사용하라 | ||
|
||
> 순수 함수 | ||
> 오직 입력만이 결과에 영향을 주는 함수 | ||
> 순수 함수를 만드는 법 | ||
> * 다른 가변 상태를 참조하지 않는다. | ||
> * 함수 스스로도 다른 상태를 변경하지 않는다. | ||
## 스트림을 올바르게 사용하는 법 | ||
|
||
아래 코드를 보자. 스트림을 올바르게 사용하지 못한 예다. | ||
```java | ||
Map<String, Long> freq = new HashMap<>(); | ||
try (Stream<String> words = new Scanner(file).tokens()) { | ||
words.forEach(word -> { | ||
freq.merge(word.toLowerCase(), 1L, Long::sum); | ||
}) | ||
} | ||
``` | ||
위 코드는 아래와 같은 이유로 스트림 코드를 가장한 반복적 코드이다. | ||
* 스트림 코드가 아닌 반복적 코드임에도 스트림을 사용했다. -> 가독성 저하 | ||
* 외부의 words의 값을 수정하는 람다를 실행했다 -> 스트림 내부의 함수가 순수 함수가 아니다. | ||
|
||
아래 코드는 위에서 언급한 문제를 해결한 코드이다. | ||
```java | ||
Map<String, Long> freq; | ||
try (Stream<String> words = new Scanner(file).tokens()) { | ||
freq = words.collect(groupingBy(String::toLowerCase, counting())); | ||
} | ||
``` | ||
* 짧고 명확해졌다. | ||
* 외부의 그 어떤 값도 참조하지 않았다.(순수 함수를 사용했다.) | ||
|
||
> forEach 연산은 스트림 결과를 보고할 때만 쓰자. | ||
> forEach 종단 연산은 for-each 반복문과 비슷하게 생겼다. | ||
> 하지만 forEach 연산은 종단 연산 중 기능이 가장 적고 덜 스트림스럽다. | ||
> 병렬화 할 수도 없다. | ||
|
||
## Collector | ||
(java.util.stream.Collectors) | ||
|
||
### Collector란? | ||
스트림의 원소들을 객체 하나에 취합하는 객체이다. | ||
|
||
> 예시 | ||
> * toList() : 스트림의 원소들을 List 객체로 취합한다. | ||
> * toSet() : 스트림의 원소들을 Set 객체로 취합한다. | ||
> * toCollection(collectionFactory) : 스트림의 원소들을 collectorFactory를 이용해서 collection으로 취합한다. | ||
```java | ||
Map<String, Long> freq; | ||
try (Stream<String> words = new Scanner(file).tokens()) { | ||
freq = words.collect(groupingBy(String::toLowerCase, counting())); | ||
} | ||
|
||
// 위에서 작성한 빈도표에서 가장 흔한 단어 10개를 뽑아내는 파이프라인 | ||
List<String> topTen = freq.keySet().stream() | ||
.sorted(comparing(freq::get).reversed()) | ||
.limit(10) | ||
.collect(toList()); | ||
``` | ||
|
||
### Collector의 예 | ||
#### toMap : 스트림의 원소들을 map으로 변환 | ||
|
||
**(1) toMap(keyMapper, valueMapper)** | ||
* key에 매핑하는 함수(keyMapper), value에 매핑하는 함수(valueMapper)를 인수로 받는다. | ||
```java | ||
private static final Map<String, Operation> stringToEnum = | ||
Stream.of(values()).collect( | ||
toMap( | ||
Object::toString, // keyMapper | ||
e -> e)) // valueMapper | ||
``` | ||
특징 | ||
* 중복 키가 존재하면 `IllegalStateException`이 발생하기 때문에 스트림의 각 원소가 고유한 키에 매핑되어 있을 때 적합하다. | ||
|
||
**(2) toMap(keyMapper, valueMapper, binaryOperator)** | ||
* key에 매핑하는 함수(keyMapper), value에 매핑하는 함수(valueMapper)를 인수로 받는다. | ||
* 중복 키가 발생할 때, binaryOperator를 통해 어떤 값을 지정할 것인지 결정한다. | ||
```java | ||
// 1번째 예시 : 음악과와 그 음악가의 베스트 앨범을 짝지은 것 | ||
Map<Artist, Album> topHits = albums.collect( | ||
toMap(Album::artist, // keyMapper | ||
a->a, // valueMapper | ||
maxBy(comparing(Album::sales))); // 중복시 최대 값을 가지도록 opeartor 구현 | ||
) | ||
|
||
// 2번 예시 : 중복 키가 생기면 마지막 값을 취하는 것 | ||
toMap(keyMapper, // keyMapper | ||
valueMapper, // valueMapper | ||
(oldVal, newVal) -> newVal); // 새로운 값을 가지도록 operator 구현 | ||
``` | ||
|
||
**(3) toMap(keyMapper, valueMapper, binaryOperator, mapFactory)** | ||
* key에 매핑하는 함수(keyMapper), value에 매핑하는 함수(valueMapper)를 인수로 받는다. | ||
* 중복 키가 발생할 때, binaryOperator를 통해 어떤 값을 지정할 것인지 결정한다. | ||
* mapFactory를 통해 map의 특정 구현체를 지정할 수 있다. | ||
|
||
```java | ||
Map<Artist, Album> topHits = albums.collect( | ||
toMap(Album::artist, // keyMapper | ||
a -> a, // valueMapper | ||
maxBy(comparing(Album::sales)) // 중복시 최대 값을 가지도록 opeartor 구현 | ||
ConcurrentHashMap::new)); | ||
) | ||
|
||
``` | ||
|
||
#### groupingBy : 특정 값으로 그룹핑 해서 스트림의 원소들을 key와 value로 반환 | ||
|
||
**(1) groupingBy - 인자 1개** | ||
인자에 분류 기준을 넣어주면 List 형식으로 값을 반환한다. | ||
```java | ||
List<Integer> integers = List.of(1, 1, 1, 2, 3, 4, 4, 5); | ||
Map<Integer, List<Integer>> collect = integers.stream().collect( | ||
groupingBy(integer -> integer) // 분류기 : 같은 숫자끼리 grouping한다. | ||
); | ||
|
||
// collect 출력 결과 : collect = {1=[1, 1, 1], 2=[2], 3=[3], 4=[4, 4], 5=[5]} | ||
``` | ||
|
||
**(2) groupingBy - 인자 2개** | ||
인자에 분류 기준을 넣어주고 다운스트림 수집기를 넣어서 리스트 이외의 type을 가질 수 있다. | ||
> 다운 스트림 | ||
> : 모든 원소를 담은 스트림으로부터 값을 생성하는 일 | ||
> ex) counting() : 스트림의 값들을 모두 센다. | ||
```java | ||
List<Integer> integers = List.of(1, 1, 1, 2, 3, 4, 4, 5); | ||
Map<Integer, Long> collect = integers.stream().collect( | ||
groupingBy(integer -> integer, // 분류기 | ||
counting())); // 다운스트림 수집기 | ||
|
||
// collect 출력 결과 : collect = {1=3, 2=1, 3=1, 4=2, 5=1} | ||
``` | ||
|
||
**(3) groupingBy - 인자 3개** | ||
맵 팩터리를 지정하여 맵의 구체적인 구현체도 설정할 수 있다. | ||
> 맵 팩터리 | ||
> 반환하는 맵의 구현체를 지정할 수 있다. | ||
```java | ||
List<Integer> integers = List.of(1, 1, 1, 2, 3, 4, 4, 5); | ||
ConcurrentHashMap<Integer, Long> collect = integers.stream().collect( | ||
groupingBy(integer -> integer, // 분류기 | ||
ConcurrentHashMap::new, // 맵 팩터리 | ||
counting())); // 다운스트림 수집기 | ||
|
||
// collect 출력 결과 : collect = {1=3, 2=1, 3=1, 4=2, 5=1} | ||
``` | ||
|
||
#### 기타 | ||
Collectors에 정의되어 있지만, '수집'과는 관련이 없는 메서드이 있다. | ||
즉, toSet(), toList()처럼 스트림의 값들이 하나의 컬렉션으로 묶이는 것이 아니라 | ||
counting()과 같이 하나의 값으로 합쳐지는 다운스트림들이다. | ||
|
||
(1) minBy() : 인수로 받은 비교자를 이용해, 스트림에서 가장 작은 원소를 찾아 반환 | ||
(2) maxBy() : 인수로 받은 비교자를 이용해, 스트림에서 가장 큰 원소를 찾아 반환 | ||
(3) joining() : 단순히 원소들을 연결한다. `["1", "2", "3"] -> "123" ` |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,138 @@ | ||
# [item 47] 반환 타입으로는 스트림보다 컬렉션이 낫다. | ||
|
||
### 스트림은 반복을 지원하지 않는다. | ||
아래 예시를 보자. | ||
```java | ||
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6); | ||
for (Integer i : integerStream::iterator) { | ||
System.out.println(i); | ||
} | ||
``` | ||
Stream은 iterator를 참조하기 때문에 이를 사용해서 for-each 문을 사용할 수 있을 것 같지만, 아래와 같이 오류가 발생한다. | ||
![stream_iterator.png](stream_iterator.png) | ||
|
||
이를 해결하기 위해서는 형변환이 필요하다. | ||
```java | ||
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6); | ||
for (Integer i : (Iterable<Integer>) integerStream::iterator) { | ||
System.out.println(i); | ||
} | ||
``` | ||
이런 형변환은 번거롭고 직관성이 떨어진다. | ||
때문에 공개 API를 구현할 때는 iterable과 stream을 모두 반환할 수 있도록 만들자. | ||
|
||
### Collection을 사용해야 하는 이유 | ||
자바에서도 모두 반환하는 예시가 있다. 그것이 바로 Collection이다. | ||
Collection 인터페이스는 iterable의 하위 타입이고 stream 메서드도 제공한다. | ||
즉, 반복과 스트림을 동시에 지원한다. | ||
때문에 원소 시퀀스를 반환하는 공개 API 반환 타입에는 Collection이나 그 하위 타입을 쓰는 것이 일반적으로 최선이다. | ||
|
||
|
||
### stream과 iterable을 모두 제공하는 API를 만드는 법 - 어댑터 사용 | ||
이전에 봤던 코드를 다시 보자. | ||
```java | ||
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6); | ||
for (Integer i : (Iterable<Integer>) integerStream::iterator) { | ||
System.out.println(i); | ||
} | ||
``` | ||
이 코드는 직관성이 떨어지고 형변환을 해줘야 하는 번거로움이 있다. | ||
|
||
이를 해결하기 위해서, Stream을 Iterable로 만들어 주는 어댑터를 만들 수 있다. | ||
```java | ||
public static <E> Iterable<E> iterableOf(Stream<E> stream) { | ||
return stream::iterator; | ||
} | ||
|
||
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6); | ||
for (Integer i : iterableOf(integerStream)) { // iterableOf 어댑터 사용 | ||
System.out.println(i); | ||
} | ||
``` | ||
|
||
만약 Iterable을 Stream으로 바꿔주고 싶으면 어떻게 할까? | ||
이 또한 어댑터를 만들어주면 된다. | ||
```java | ||
public static <E> Stream<E> streamOf(Iterable<E> iterable) { | ||
return StreamSupport.stream(iterable.spliterator(), false); | ||
} | ||
|
||
List<Integer> integers = List.of(1, 2, 3, 4, 5); // 스트림 사용 | ||
streamOf(integers).filter(integer -> integer == 1).collect(Collectors.toSet()); | ||
``` | ||
|
||
### Collection의 원소들을 다 올리기엔 너무 원소의 크기가 크다면? | ||
전용 컬렉션을 구현하는 방안을 생각해보자! | ||
|
||
**예1) 집합의 멱집합을 구해서 컬렉션으로 반환하는 경우** | ||
> 멱집합 : 집합의 부분집합을 원소로 갖는 집합 {1,2} -> {공집합, {1}, {2}, {1,2}} | ||
집합의 원소가 n개이면 멱집합의 원소의 개수는 2^n이다. 때문에 멱집합을 표준 컬렉션 구현체에 저장하는 생각은 위험하다. | ||
때문에 아래처럼 각 원소의 인덱스를 비트 벡터로 사용하는 전용 컬렉션 구현체를 만들 수 있다. | ||
```java | ||
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); | ||
} | ||
|
||
// 인덱스 n 번째 비트 값 : 해당 원소가 원래 집합의 n 번째 원소를 포함하는지 여부 | ||
@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; | ||
} | ||
}; | ||
} | ||
} | ||
``` | ||
|
||
**예2) 입력 리스트의 모든 부분 리스트 반환하는 경우** | ||
아래와 같이 이중 포문을 사용할 수 있지만, n^2의 시간 복잡도가 발생한다. 이는 메모리 차지가 발생한다. | ||
```java | ||
for (int start = 0; start < src.size(); start++) { | ||
for (int end = start + 1; end <= src.size(); end++) { | ||
System.out.println(src.subList(start, end)); | ||
} | ||
} | ||
``` | ||
|
||
컬렉션 대신, 스트림을 반환하면 다음과 같다. | ||
```java | ||
public class SubLists { | ||
// 컬렉션 대신, 스트림을 반환 | ||
public static <E> Stream<List<E>> of(List<E> list) { //(a, b, c) | ||
return Stream.concat(Stream.of(Collections.emptyList()), | ||
prefixes(list).flatMap(SubLists::suffixes)); | ||
} | ||
|
||
private static <E> Stream<List<E>> prefixes(List<E> list) { // (a), (a,b), (a,b,c) | ||
return IntStream.rangeClosed(1, list.size()) | ||
.mapToObj(end -> list.subList(0, end)); | ||
} | ||
|
||
private static <E> Stream<List<E>> suffixes(List<E> list) { // (a,b,c), (b,c), (c) | ||
return IntStream.range(0, list.size()) | ||
.mapToObj(start -> list.subList(start, list.size())); | ||
} | ||
} | ||
``` | ||
|
||
### 결론 | ||
> 반환 전부터 이미 원소들을 컬렉션에 담아 관리하고 있거나 원소 개수가 적다 -> 표준 컬렉션 | ||
> 원소들이 너무 많아서 관리가 어렵다면 -> 전용 컬렉션을 구현할 수도 있다. | ||
> 컬렉션 반환이 불가능 하다면 -> stream 또는 Iterable | ||
> 되도록이면, API는 stream과 iterable을 반환하도록 하자 |