Stream API
항상 stream이 나이스한건 아니다. for가 더 나이스한 경우가 분명히 존재한다.
구구절절 적기는 좀 애매한데… 아무튼 코드 짜다 보면 느껴질 때가 있음.
stream API의 최종 연산
1
2
3
4
5
6
7
8
9
10
11
| 1. 요소의 출력 : forEach(), forEachOrdered() {병렬연산으로 순서 틀어질 때}
2. 요소의 소모 : reduce()
3. 요소의 검색 : findFirst(), findAny()
4. 요소의 검사 : anyMatch(), allMatch(), noneMatch()
5. 요소의 통계 : count(), min(), max()
6. 요소의 연산 : sum(), average()
7. 요소의 수집 : toArray(String[]::new), .collect(Collectors.toList()) Collectors.toMap() 같은 것.
Collectors.joining(", ", "<", ">") 도 유용하다.
Collectors.collectingAndThen()
summaryStatistics() 같은 것도 있다. 한꺼번에 count, sum, average 같은 것...
|
flatMap
1
2
3
4
| Stream.of(values())
.flatMap(e -> e.codeList.stream()) // < 이렇게 반환타입 stream으로.
.collect(Collectors.toList());
|
groupingBy
1
2
3
4
5
6
7
8
| // mapFactory TreeMap으로 순서 보장 맵 얻기
SortedMap<String, List<DivisionRateSingle>> groupedRateSingle = rateSingles.stream()
.collect(Collectors.groupingBy(drs -> drs.getServiceId() + drs.getCorpId(), TreeMap::new, Collectors.toList()));
// 하지만 sorting 은 다음과 같이 하는 것이 더 낫다.
// 단순 String 몇 개 비교는 Comparable 구현 할 필요 없이, 다음을 사용하면 됨.
.sorted(Comparator.comparing(d -> (d.getServiceId() + d.getCorpId())))
words.collect(groupingBy(word -> alphabetize(word)) 같은 것도 가능!
|
partitioningBy
stream을 true false에 따라 간단히 2개로 구분 할 때. (filter로 2번 안돌려도 된다!) 같은 기능이 groupingBy로도 가능은 하지만 이걸 쓰는게 더 직관적이다.
reduce
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| String result2 = stream2.reduce("시작", (s1, s2) -> s1 + "++" + s2);
// 초기값과 iter 변수의 타입이 다른 경우 3개의 인자가 필요함.
String str = "TEST_{a}_{b}_c";
Map<String, String> map = ImmutableMap.<String, String>builder()
.put("{a}", "A대응")
.put("{b}", "B대응")
.build();
String result = map.entrySet().stream()
.reduce(
str,
(acc, e) -> StringUtils.replace(acc, e.getKey(), e.getValue()),
(oldStr, newStr) -> newStr
);
|
java.util.stream.Collectors
1
2
3
4
5
6
7
8
9
10
| // 단어를 grouping하여 몇 번 등장했는지를 value로.
Map<String, Long> freq = words.collect(groupingBy(String::toLowerCase, counting()));
// 빈도표에서 가장 등장 횟수가 많은 상위 10개 뽑아내기
List topTen = freq.keySet().stream()
.sorted(comparing(freq::get).reversed())
.limit(10)
.collect(toList());
|
1
2
3
4
5
6
7
8
| // 각 키와 해당 키의 특정 원소를 연관 짓는 맵을 생성하는 수집기
Map<Artist, Album> topHits = albums.collect(
toMap(Album::artist, a->a, maxby(comparing(Album::sales))));
// 마지막에 쓴 값을 취하는 수집기 형태
toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)
|
자바는 정적 타입 언어라 난감한 상황이 발생할 수 있다.
1
2
3
4
5
6
| List<ChangeResult> changeResults = Arrays.stream(PartnerCode.values())
.map(partner -> getPartnerService(partner.getPartnerCode())) // 여기서 이미 partner에 대한 정보는 없고 Service만 다음 연산으로 넘어간다.
.map(service -> service.change(userCi, point, PointChangeService.CHANGE\_ALL)) // 이 단계에서 ChangeResult만 있고, partner에 대한 정보는 없다.
.filter(result -> result.getStatus() != CommonStatus.LIMIT\_EXCEEDED)
.collect(Collectors.toList()); // 하지만 최종적으로는 partner와 그에 대한 ChangeResult를 묶은 정보가 필요하다.
|
최종적으로 필요한건 partner,ChangeResult를 서로 묶은 결과인데, 중간에 map을 적용하면서 partner에 대한 정보가 사라진다. 동적 타입 언어라면 그냥 람다에서 2개 리턴하면 간단하게 끝나는 문제이지만, 자바에서는 또 new AbstractMap.Entity어쩌구… 해야해서 코드가 매우 더러워지고, 코드가 더러워지면 가독성이 매우 떨어져 함수형의 간결함이라는 장점을 버려야 한다. 해결법은 크게 3가지다.
- new Pair같은걸로 최종 연산까지 끌고 가는 방법. 위에 설명했듯 가독성이 떨어진다.
- 반복이 크지 않다면 stream 연산을 두개로 쪼개는 방법.
- ChangeResult를 Wrapping해서 ChangeResultWithPartner를 리턴하는 메서드를 작성하는 방법.
- peek()으로 외부 변수를 건드려서 중간에 밖으로 빼내는 방법도 있다. side-effect가 발생하게 되므로 좋은 방법은 아니다.
How to call setter in chain of Stream (stream 안에서 collection 원소의 setter 사용?)
1
2
3
4
5
6
7
| List<Foo> newFoos = foos.stream()
.map(foo -> {
foo.setTitle("Some value");
return foo;
}))
.collect(Collectors.toList()); // 이 종단 연산 없으면 아예 map도 안탄다는거.
|
- 이렇게 쓰면 foos라는 원본 컬렉션의 데이터들에 set 하게 되는 것이라서, 원본 데이터가 훼손됨. functional한 방법은 아니다.
- map을 peek으로 바꾸면 return을 뺄 수 있는데, 즉 peek으로 원본 데이터를 건드리는 것과 같은 느낌이라는 것이다. 그리고 진짜 원본 데이터를 바꾸고자 하는 의도라면 map이나 peek이 아니라
.forEach()
를 사용해야 의도가 드러난다. - 원본 데이터를 절대 건드리고 싶지 않다면?
- map 안에서 new Foo() 해서 반환하는 방법이 있기는 한데… 이러면 Foo에 자기 자신을 받는 constructor를 만들어 줘야 일처리가 간단해져서, 깔끔하지 못하다
- 그나마 foos를 deep copy한 새로운 배열을 반환 받은 다음에 여기서 stream을 태우면 원본 데이터는 보존할 수 있는데, deep copy를 써야 한다는 마음의 짐이 있음 ㅠㅠㅋㅋ
- 그래서 이런 경우 그냥
for(:)
를 쓰거나, 스트림을 쓰겠다면 forEach()
를 쓰는게 의도가 잘 드러나는 것 같다.- 애초에 자바같은 객체를 사용하는 객체 지향 언어에서는 상태를 변경할 수 밖에 없고, 이런 상태 변경이라는 개념은 Functional 개념과는 맞지 않는 것이기 때문에, 이처럼 약간 애매한 상황이 발생할 수 있는 것이겠지!
take? break?
take 같은 break 함수로는 skip, limit가 있고, 아래 최종 연산을 통해 다양하게 사용 가능하다. Java 9에서는 takeWhile, dropWhile 등이 추가되었다. 사실 반복문에서 break같은 효과를 주려면 takeWhile같은 API의 지원이 필수적이다 ㅜㅜ