관리 메뉴

생각해보기

모던 자바 인 액션 -4- 본문

자바

모던 자바 인 액션 -4-

정한_s 2021. 12. 8. 18:41

자바 8과 자바 9에서 스트림 API가 지원하는 다양한 연산을 알아보자

필터링

스트림의 요소를 선택하는 방법

 

프레디케이트로 필터링 - filter 메서드

프레디케이트(불리언을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.

 List<Dish> vegetarianMenu = menu.stream()
                .filter(Dish::isVegetarian) // 채식 요리인지 확인하는 메서드 참조
                .collect(toList());

 

고유 요소 필터링 - distinct 메서드

고유 요소로 이루어진 스트림을 반환하는 distinct 메서드를 지원한다.

// distinct, 짝수만 + 중복 제거
List<Integer> numbers = List.of(1,2,3,4,5,6,4);
List<Integer> evenDistinctNumber = numbers.stream()
        .filter(i -> i % 2 == 0)
        .distinct()
        .collect(toList());

스트림 슬라이싱 

스트림의 요소를 선택하거나 스킵하는 다양한 방법

 

프레디케이트를 이용할 슬라이싱 - takeWhile 메서드

정렬된 리스트가 있을 때 특정 조건에 만족하지 않았을 때 반복 작업 종료

// sortedList 0 ~ 19의 수
// takeWhile, List 정렬 되었다고 가정 8 이하의 수 반환
List<Integer> under9 = sortedList.stream()
        .takeWhile(i -> i <= 8)
        .collect(toList());

 

프레디케이트를 이용할 슬라이싱 - dropWhile 메서드

dropWhile은 takeWhile의 정반대 작업. dropWhile은 프레디케이트가 처음으로 거짓되는 지점 이전의 요소를 버린다.

// sortedList 0 ~ 19의 수
// dropWhile, List 정렬되었다고 가정 8 초과의 수 반환
List<Integer> upper8 = sortedList.stream()
        .dropWhile(i->i<=8)
        .collect(toList());

 

스트림 축소 - limit 메서드

주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 메서드이다. 컬렉션 요소(인덱스) 순서대로 반환한다.

// sortedList 중 가장 좌측부터 3개 뽑기
List<Integer> left3 = sortedList.stream()
        .limit(3)
        .collect(toList());

 

요소 건너뛰기 - skip 메서드

처음 n개 요소를 제외한 스트림을 반환하는 메서드이다.

// sortedList 중 가장 좌측부터 3개를 제외한 뽑기 (0,1,2...19 이므로 0,1,2 제외한 3,4,...19 뽑기)
List<Integer> start3 = sortedList.stream()
        .skip(3)
        .collect(toList());

 매핑

특정 객체에서 특정 데이터로 선택하는 기능

 

스트림의 각 요소에 함수 적용하기 - map 메서드

함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각 요소에 적용되며 함수에 적용한 결과가 새로운 요소로 매핑된다.

// 메뉴의 각 요리명의 길이 추출하기
// menu는 List<Dish> 이다
List<Integer> dishName = menu.stream()
        .map(Dish::getName) // 각 Dish 객체에서 이름만 추출한다, Stream<Dish> -> Stream<String>
        .map(String::length) // 메뉴의 이름에서 길이를 추출한다, Stream<String> -> Stream<Integer>
        .collect(toList());

 

스트림의 평면화 - flatMap 메서드

flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다. 즉, 각각의 스트림을 모아 하나의 스트림으로 연결한다.

ex) Stream<Stream<String>>  ==>  Stream<String> Stream들을 하나의 스트림으로 연결

Stream<Stream<Stream<String>>>  -> Stream<Stream<String>> ==> 계층적 구조면 하나의 계층만 통합한다  

// words 리스트에서 고유문자를 반환하기
List<String> words = List.of("Hello","World");
List<String> disticntS = words.stream().map(s -> s.split("")) // -> Stream<String[]>
        .flatMap(Arrays::stream) 
        .distinct()
        .collect(toList());

 

 

// 세 리스트의 순서쌍 만들기
List<Integer> number1 = List.of(1,2,3);
List<Integer> number2 = List.of(3,4);
List<Integer> number3 = List.of(5,6);
List<int[]> triple = number1.stream()
        .flatMap(i -> number2.stream() //  
                .flatMap(j -> number3.stream()
                        .map(k -> new int[]{i, j, k}
                        )
                )
        )
        .collect(toList());

 

검색과 매칭

특정 속성이 데이터 집합에 있는지 여부를 검색하는 데이터 처리

 

프레디케이트가 적어도 한 요소와 일치하는지 확인 -  anyMatch

주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때 사용한다. 조건에 일치하면 true 리턴한다

// sortedList 0 ~ 19의 수
// 10 보다 적은 수가 있는 지 확인
boolean under10 = sortedList.stream()
        .anyMatch(i -> i < 10);

 

프레디케이트가 적어도 모든 요소와 일치하는지 검사 - allMatch

스트림의 모든 요소가 프레디케이트와 일치하는 지 검사한다. 모든 요소가 조건에 일치하면 true 리턴한다

// sortedList 0 ~ 19의 수
// 모든 수가 적은 수가 11보다 작은 지 확인 => 만족하지 않는 수가 있으므로 false 리턴 
boolean under11 = sortedList.stream()
        .allMatch(i -> i < 11);

 

주어진 프레디케이트와 일치하는 요소가 없는지 확인 - noneMatch

allMatch의 반대 연산을 수행한다. 일치하는 요소가 있으면 false를 리턴한다

// sortedList 0 ~ 19의 수
// 10 보다 적은 수가 있는 지 확인 => 있으므로 false 리턴
boolean under10None = sortedList.stream()
        .noneMatch(i -> i < 10);

 

anyMatch, allMatch, noneMatch, findFirst, findAny등의 메서드는 자바의 &&, ||과 같이 쇼트서킷 기법을 사용한다.

* 쇼트서킷 : 전체 연산을 수행하지 않았더라도 원하는 요소를 찾았으면 즉시 결과를 반환하는 기법

 

요소 검색 - findAny

현재 스트림에서 임의의 요소를 반환한다.  Optional 클래스로 감싸져서 반환된다.

// 임의의 요소 반환
// sortedList에서 5 보다 작은 임의의 요소 반환
Optional<Integer> any = sortedList.stream()
        .filter(i->i<5)
        .findAny();

 

첫 번째 요소 검색 - findFirst

정렬된 연속 데이터로부터 생성된 스트림에는 논리적인 아이템 순서가 정해져있을 수 있다. findFirst 메소드는 이런 스트림에서 첫 번째 요소를 찾을때 사용한다.

// 첫번째 요소 반환
// sortedList에서 5 보다 작은 첫번째 요소 반환
Optional<Integer> any = sortedList.stream()
        .filter(i->i<5)
        .findAny();


* 병렬 실행에서는 첫 번째 요소를 찾기가 힘들다.

요소 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.

요소 반환 순서가 중요하다면 findFirst를 사용한다.

* Optional 이란?

더보기

Optional<T> 클래스는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스이다.

위 예제에서 findAny는 아무 요소도 반환하지 않을 수 있기 때문에 null을 방지하고자 사용되었다.

 

Optional의 메소드

  • isPresent() : Optional이 값을 포함하면 true, 포함하지 않으면 false를 반환
  • isPresent(Consumer<T> b   lock) : 값이 있으면 주어진 블록을 실행
  • T get() : 값이 존재하면 반환, 없으면 NoSuchElementException을 발생
  • T orElse(T other) : 값이 있으면 반환, 없으면 기본값을 반환

 

리듀싱

모든 스트림의 요소를 처리해서 값으로 도출해내는 리듀싱 연산 기능

 

요소의 합 - reduce 메서드

reduce는 두 개의 인수를 갖는다.

  • 초깃값
  • 스트림의 두 요소를 합쳐서 하나의 값으로 만드는 데 사용할 람다
// 요소의 합
// 리스트의 총합 구하기
List<Integer> numberList = List.of(1,2,3,4);
// 초기값 있는 경우, Integer -> int, auto Unboxing
int reduce = numberList.stream().reduce(0, (a, b) -> a + b);
 // 초기값 없는 경우
Optional<Integer> reduceWithOutInit = numberList.stream().reduce((a, b) -> a + b);

* 초깃값이 없는 경우

스트림에 아무 요소도 없다면 초깃값이 없으므로 reduce는 합계를 반환할 수 없다. 따라서 초기값이 없는 reduce는 Optional 객체를 반환한다.


reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행한다.

이를 통해 최댓값과 최솟값을 찾을 때도 reduce를 활용할 수 있다.

// 리스트 최대값 구하기
Optional<Integer> max = numberList.stream().reduce(Integer::max);

 

reduce 메서드의 장점과 병렬화

단계적 반복으로 합계를 구할때는 sum 변수를 공유해야 하므로 쉽게 병렬화가 어렵다.

하지만 reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.

물론 병렬로 실행하기 위해서는 연산이 어떤 순서로 실행되더라도 결과가 바뀌지 않는 구조여야 한다.

 

스트림 연산 : 상태 있음과 상태 없음
스트림 연산은 각각 다양한 작업을 수행한다. 따라서 각각의 연산은 내부적인 상태를 고려해야한다.

 

map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
따라서 이들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산(stateless operation)이다.

reduce, sum, max 같은 연산은 결과를 누적할 내부 상태가 필요하다. 하지만 내부 상태는 int, double 등과 같이 작은 값이며, 스트림에서 처리하는 요소 수와 관계없이 한정(bounded)되어있다.

반면 sorted나 distinct 같은 연산을 수행하기 위해서는 과거의 이력을 알고있어야 한다. 예를 들어 어떤요소를 출력 스트림으로 추가하려면 모든 요소가 버퍼에 추가되어 있어야 한다. 따라서 데이터 스트림의 크기가 크거나 무한이라면 문제가 생길 수 있다. 이러한 연산을 내부 상태를 갖는 연산(stateful operation)이라 한다.

 

중간 연산자와 최종연산 정리

연산 형식  반환 형식 사용된 함수형
인터페이스 형식
사용된 함수
디스크립터
filter 중간 연산 Stream<T> Predicate<T> T->boolean
dictinct 중간 연산
(상태 있는 언바운드)
Stream<T>    
takeWhile 중간 연산 Stream<T> Predicate<T> T->boolean
dropWhile 중간 연산 Stream<T> Predicate<T> T->boolean
skip 중간 연산
(상태 있는 바운드)
Stream<T> long  
limit 중간 연산
(상태 있는 바운드)
Stream<T> long  
map 중간 연산 Stream<R> Function<T, R> T->R
flatMap 중간연산 Stream<R> Function<T, Stream<R>> T->Stream<R>
sorted 중간 연산
(상태 있는 언바운드)
Stream<T> Comparator<T> (T, T) -> int
anyMatch 최종 연산 boolean Predicate<T> T->boolean
noneMatch 최종 연산 boolean Predicate<T> T->boolean
allMatch 최종 연산 boolean Predicate<T> T->boolean
findAny 최종 연산 Optional<T>    
findFirst 최종 연산 Optional<T>    
forEach 최종 연산 void Consumer<T> T->void
collect 최종 연산 R Collector<T,A,R>  
reduce 최종 연산
(상태 있는 바운드)
Optional<T> BinaryOperator<T> (T, T) -> T
count 최종 연산 long    

 

기본형 특화 스트림

스트림 API는 박싱 비용을 피할 수 있도록 IntStream, LongStream, DoubleStream 을 제공한다.

각각의 인터페이스는 sum, max와 같은 숫자 관련 리듀싱 연산 수행 메서드를 제공하며, 다시 객체 스트림으로 복원할 수 있는 기능도 제공한다.

 

숫자 스트림으로 매핑 - mapToInt, mapToLong, mapToDouble 메서드

Stream<T> 대신 특화된 스트림을 반환한다.

// 예시
int calories = menu.stream().mapToInt(Dish::getCalories).sum();

mapToInt 메서드는 각 요리에서 모든 칼로리(Interger형식)를 추출한 다음에 IntStream을(Stream<Integer>가 아님) 반환한다. 스트림이 비어있으면 sum은 기본값 0을 반환한다.

특화 스트림은 다양한 유틸리티 메서드를 지원한다(min, max, average 등)

 

객체 스트림으로 복원하기 - boxed 메서드

boxed 메서드를 이용해서 특화 스트림을 일반 스트림으로 변환할 수 있다.

IntStream intStream = menu.stream().mapToInt(Dish::getCalories); 
Stream<Integer> stream = intStream.boxed();

기본값: OptionalInt

Optional을 Integer, String 등의 참조 형식으로 파라미터화할 수 있으며, OptionalInt, OptionalDouble, OptionalLong 세 가지 기본형 특화 스트림 버전도 제공한다.

// 예시
OptionalInt maxCalories = menu.stream().mapToInt(Dish::getCalories).max();

숫자 범위 - range, rangeClosed

IntStream과 LongStream에서는 range와 rangeClosed라는 두 가지 정적 메서드를 제공한다.

두 메서드 모두 시작값과 종료값을 인수로 가진다.

  • range 메서드는 시작값은 결과에 포함되고, 종료값이 결과에 포함되지 않는다.
  • rangeClosed는 시작값과 종료값이 결과에 포함된다.

스트림 만들기

다양한 방식으로 스트림을 만들 수 있다

 

값으로 스트림 만들기 - of

임의의 수를 인수로 받는 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.

Stream<String> stream = Stream.of("Modern", "Java", "In", "Action");

 

스트림 비우기 - empty

empty 메서드를 이용해서 스트림을 비울 수 있다.

Stream<String> emptyStream = Stream.empty();

 

null이 될 수 있는 객체로 스트림 만들기 - ofNullable

ofNullable 메서드로 null이 될 수 있는 객체를 포함하는 스트림을 만들 수 있다.

이 패턴은 flatMap과 함께 사용하는 상황에서 유용하다.

// 예시
Stream<String> stream = Stream.ofNullable(System.getProperty("home")); 

Stream<String> values = Stream.of("config", "home", "user")
	.flatMap(key -> Stream.ofNullable(System.getProperty(key)));

 

배열로 스트림 만들기 - Arrays.stream

배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림을 만들 수 있다.

int[] numbers = {2, 3, 5, 7, 11, 13}; 
int sum = Arrays.stream(numbers).sum();

 

 

파일로 스트림 만들기 - java.nio.file.Files 정적 메서드에서 지원

파일을 처리하는 등의 I/O 연산에 사용하는 자바의 NIO API(비블록 I/O)도 스트림 API를 활용할 수 있다. 

Files.lines로 파일의 각 행 요소를 반환하는 스트림을 얻을 수 있다.

Stream 인터페이스는 AutoCloseable 인터페이스를 구현하므로, try 블록 내의 자원은 자동으로 관리된다.

long uniqueWords = 0; 
// 스트림은 자원을 자동으로 해제 할수 있는 AutoCloseable 이므로 try-finally가 필요없다
try(Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {

  uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
          .distinct()
          .count();
}
catch (IOException e){

}


함수로 무한 스트림 만들기 Stream.iterate, Stream.generate
Stream.iterate와 Stream.generate를 이용해서 함수에서 스트림을 만들 수 있다.

두 연산을 이용하면 무한 스트림, 즉 고정된 컬렉션에서 고정된 크기로 스트림을 만들었던 것과 달리 크기가 고정되지 않은 스트림을 만들 수 있다.

 

iterate 메서드

Stream.iterate(0, n -> n + 2)
	.limit(10)
	.forEach(System.out::println);

iterate 메서드는 초깃값과 람다(UnaryOperator<T>)를 인수로 받아서 새로운 값을 끊임없이 생산할 수 있다.

예제에서는 람다 n -> n+2, 즉 이전 결과에 2를 더한 값을 반환한다.

iterate는 요청할 때마다 값을 생산할 수 있으며 끝이 없으므로 무한 스트림을 만든다. 이러한 스트림을 언바운드 스트림이라고 표현한다.

 

iterate 메서드는 프레디케이트를 지원한다. 두 번째 인수로 프레디 케이트를 받아 작업 중단의 기준으로 사용한다.

0에서 시작해서 100보다 크면 숫자 생성을 중단하는 코드를 다음처럼 구현할 수 있다.

IntStream.iterate(0, n -> n < 100, n -> n + 4).forEach(System.out::println);

filter 메서드는 언제 이 작업을 중단해야 하는지 알 수 없으므로 takeWhile을 사용해야 한다

IntStream.iterate(0,n->n+4)
        .takeWhile(n->n<100)
        .forEach(System.out::println);

generate 메서드

iterate와 달리 generate는 생산된 각 값을 연속적으로 계산하지 않으며, Supplier<T>를 인수로 받아서 새로운 값을 생산한다.

DoubleStream.generate(Math::random)
        .limit(5)
        .forEach(System.out::println);

 

'자바' 카테고리의 다른 글

모던 자바 인 액션 -6-  (0) 2021.12.10
모던 자바 인 액션 -5-  (0) 2021.12.10
모던 자바 인 액션 -3-  (0) 2021.12.08
모던 자바 인 액션 -2-  (0) 2021.12.07
모던 자바 인 액션 -1-  (0) 2021.12.06
Comments