Modern Java In Action 정리
Modern Java In Action을 읽고 내용을 정리해본다.
5장 스트림 활용
필터링
filter()
메서드는Predicate<T>
를 인자로 일치하는 모든 요소를 포함하는 스트림을 반환한다.@Test public void 스트림_filter(){ List<Integer> numbers = Arrays.asList(1, 2, 3, 1, 2, 4); numbers.stream() .filter(i -> i % 2 == 0) // 짝수만 필터링 .distinct() // 중복요소 제거, hashCode와 equals로 결정된다. .forEach(System.out::println); // 출력 }
distinct()
,skip(n)
,limit(n)
와 같이 사용되어 스트림을 축소할 수 있다. 직관적으로 동작이 메서드명에 나타나므로 따로 정리를 하지 않는다.- 자바9에서 추가된
takeWhile()
,dropWhile()
를 활용하면 기본 filter에서 추가 동작을 지정할 수 있다.
(이미 정렬된 상태에서 유용하게 적용이 가능하다.)
@Test
public void 스트림_takeWhile_dropWhile(){
// 메뉴 컬렉션을 칼로리를 기준으로 오른차순 정렬
menu.sort(Comparator.comparing(Dish::getCalories));
menu.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));
// takeWhile() : 조건에 만족할 때까지 스트림을 반환, false시 반복 중단
List<Dish> lowCalDish = menu.stream()
.takeWhile(dish -> dish.getCalories() < 450)
.collect(Collectors.toList());
System.out.println("\n450 칼로리 미만 ------");
lowCalDish.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));
// dropWhile() : 조건에 만족하는 요소까지 버림, true부터 스트림을 반환
List<Dish> highCalDish = menu.stream()
.dropWhile(dish -> dish.getCalories() < 450)
.collect(Collectors.toList());
System.out.println("\n450 칼로리 이상 ------");
highCalDish.stream().forEach((dish -> System.out.print(dish.getName() + "(" + dish.getCalories() + ") ")));
}
chicken(400) salmon(450) french fries(530) beef(700) pork(800)
450 칼로리 미만 ------
chicken(400)
450 칼로리 이상 ------
salmon(450) french fries(530) beef(700) pork(800)
매핑
- 스트림API는 스트림의 요소에서 다른 요소로 변환할 수 있는 map()과 flatMap() 메서드를 제공한다.
<R> Stream<R> map(Function<? super T, ? extends R> var1);
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> var1);
- 매핑 메서드들은 스트림의 한 요소(T 타입)에서 맵핑되는 새로운 타입(R 타입)의 요소를 만들어 반환한다.
- 아래 예시에서는
map(Function<String, Integer>)
를 이용하여 Integer를 요소로 갖는 스트림을 생성하고 이를 List로 반환했다.
@Test
public void map_테스트(){
List<String> strs = List.of("하이", "헬로우");
// 문자열 길이를 요소로 같는 리스트 생성
List<Integer> lengths = strs.stream()
.map(String::length)
.collect(Collectors.toList());
}
- map()은 스트림의 요소별로 1대1로 반환하기 때문에 이해가 쉽지만 flatMap()은 다른 동작을 한다.
- map()과 flatMap()에 파라미터로 주는 Function 인터페이스를 보면 반환형이 다른것을 볼 수 있다.
- map() :
Function<? super T, ? extends R>
- flatMap() :
Function<? super T, ? extends Stream<? extends R>>
- map() :
- flatMap의 파라미터인 Function 인터페이스의 구현체는 반환하는 객체의 타입이 Stream 타입이고, flatMap은 Stream 그 자체가 아닌 Stream의 컨텐츠로 매핑한다. 즉, map과 다르게 flatMap은 하나의 평면화된 스트림을 반환한다.
- 차이를 볼 수 있게 예제 코드를 확인한다.
문자열 리스트에서 중복을 제거한 한글자가 요소인 리스트를 구한다.@Test public void flatMap_테스트(){ List<String> strs = List.of("HI", "HELLO"); List<String> distinctStrs = strs.stream() .map(s -> s.split("")) // Stream<String[]> // .map(sArr -> Arrays.stream(sArr)) // Stream<Stream<String>> .flatMap(sArr -> Arrays.stream(sArr)) // Stream<String> .distinct() .collect(Collectors.toList()); distinctStrs.forEach(System.out::println); // H I E L O }
sArr -> Arrays.stream(sArr) 의 실행결과인 Stream
- map은 Stream<Stream
>을 리턴
- flatMap은 Stream
의 컨텐츠인 String으로 하나의 평면화면 Stream 을 리턴했다. distinct()도 의도한 대로 글자당 중복을 제거하도록 동작한다.
- 이 밖에도 flatMap은 아래와 같이 변환을 지원하므로 자세한 내용은 Mkyong flatMap Example 의 내용을 참고하자
Stream<String[]> -> flatMap -> Stream<String> Stream<Set<String>> -> flatMap -> Stream<String> Stream<List<String>> -> flatMap -> Stream<String> Stream<List<Object>> -> flatMap -> Stream<Object>
검색과 매칭
- 스트림API는 특정 속성이 데이터 집합에 있는지 여부를 검색하는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.
@Test
public void 검색과_매칭(){
List<String> strs = List.of("HI", "HELLO");
boolean isAllStartWithH = strs.stream()
.allMatch(s -> s.startsWith("H"));
System.out.println("모두 H로 시작합니다. : " + isAllStartWithH);
boolean isOneEndWithO = strs.stream()
.anyMatch(s -> s.endsWith("O"));
System.out.println("한 요소 이상이 O로 끝납니다. : " + isOneEndWithO);
boolean isNoneEndWithK = strs.stream()
.noneMatch(s -> s.endsWith("K"));
System.out.println("모두 K로 끝나지 않습니다. : " + isNoneEndWithK);
Optional<String> first = strs.stream().findFirst();
System.out.println("첫번째 요소는(First) : " + first.get());
Optional<String> any = strs.stream().findAny();
System.out.println("첫번째 요소는(Any) : " + any.get());
}
- findFirst와 findAny가 모두 필요한 이유는 병렬성 때문이다. 요소의 반환순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
리듀싱
- 최종 결과값이 나올 때까지 스트림의 요소를 반복적으로 처리하는 연산을 리듀싱 연산이라고 한다.
- 리듀싱 연산은 reduce 메서드로 수행할 수 있으며 사용은 예제코드를 참고한다.
@Test public void reduce(){ int[] nums = new int[]{1, 2, 3, 4, 5}; // 초기값을 사용하여 reduce 연산 int sumWithInitVal = Arrays.stream(nums) .reduce(0, Integer::sum); System.out.println("합계 : " + sumWithInitVal); // 15 // 초기값 없이 reduce 연산 OptionalInt optSum = Arrays.stream(nums) .reduce(Integer::sum); System.out.println("합계 : " + optSum.getAsInt()); // 15 // 초기값이 없기 때문에 reduce 연산시 결과값이 없을 수 있다. OptionalInt optSum2 = Arrays.stream(new int[]{}) .reduce(Integer::sum); System.out.println("합계 : " + optSum2.orElse(0)); // 0 }
스트림API에서 지원하는 연산에 대한 정리