Post

(Java) Stream API 노트

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가지다.

  1. new Pair같은걸로 최종 연산까지 끌고 가는 방법. 위에 설명했듯 가독성이 떨어진다.
  2. 반복이 크지 않다면 stream 연산을 두개로 쪼개는 방법.
  3. ChangeResult를 Wrapping해서 ChangeResultWithPartner를 리턴하는 메서드를 작성하는 방법.
  4. 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의 지원이 필수적이다 ㅜㅜ

This post is licensed under CC BY 4.0 by the author.