자바 뚝딱거리기

[JAVA] 스트림 활용

bimppap 2021. 3. 5. 16:21

스트림 API 가 지원하는 다양한 연산을 살펴보자

1. 필터링

- filter : Predicate 로 필터링

List<Beverage> beverageMenu = menu.stream()
                                  .filter(Beverage::isTea) // 음료 종류가 차인지 확인하는 메소드 참조
                                  .collect(toList());
Predicate
boolean을 반환하는 함수

 

- distinct : 고유요소 필터링

List<Beverage> beverageMenu = menu.stream()
                                  .filter(Beverage::isTea)
                                  .distinct()              // 중복을 '필터링'
                                  .collect(toList());

 

2. 슬라이싱

- takeWhile, dropWhile : Predicate 를 이용한 슬라이싱 (JAVA 9)

filter 는 전체 스트림을 반복하면서 각 요소에 Predicate를 적용한다.

하지만 이미 정렬된 리스트라던가 특수한 상황일 땐 특정 조건이 나왔을 때 반복 작업을 중단할 수 있다. 요소가 많이 담긴 큰 스트림일 경우 filter를 쓸 때와 상당한 차이가 날 수 있다.

// takeWhile : Predicate를 만족하는 요소까지만 선택하고 나머진 버린다.
List<Beverage> filterdMenu
    = specialMenu.stream()
                 .takeWhile(beverage -> beverage.getSize() < SIZE.GRANDE)
                 .collect(toList());
                 
// dropWhile : Predicate를 만족하지 않는 지점까지 발견된 요소를 버리고 나머지를 선택한다.
List<Beverage> filterdMenu
    = specialMenu.stream()
                 .dropWhile(beverage -> beverage.getSize() < SIZE.GRANDE)
                 .collect(toList());

 

- limit : 스트림 축소

List<Beverage> beverageMenu = menu.stream()
                                  .filter(Beverage::isTea)
                                  .limit(3)               // 3 이하의 크기를 갖는 새로운 스트림 반환
                                  .collect(toList());

- skip : 요소 건너뛰기

List<Beverage> beverageMenu = menu.stream()
                                  .filter(Beverage::isTea)
                                  .skip(2)                // 종류가 차인 처음 두 음료를 건너뛰고 나머질 반환
                                  .collect(toList());

 

3. 매핑

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

// beverage 객체 스트림을 getName으로 매핑하여 Steam<String>을 반환
List<String> beverageNames = menu.stream()
                                  .map(Beverage::getName)
                                  .collect(toList());

- flatMap : 스트림 평면화

String words = "Hello"
words.stream()
     .map(word -> word.split(""))  // h e l l o 별개의 스트림으로 분할
     .flatMap(Arrays::stream)
     .distinct()
     .collect(toList());

 

4. 검색과 매칭

(1) 매칭

- anyMatch : Predicate 가 적어도 한 요소와 일치하는지 확인

if(menu.stream().anyMatch(Beverage::isTea)) {
	System.out.println("적어도 하나는 음료수가 차다!!");
}

- allMatch : Predicate 가 모든 요소와 일치하는지 확인

if(menu.stream().allMatch(Beverage::isTea)) {
	System.out.println("모든 음료수가 차다!!");
}

- noneMatch: Predicate가 모든 요소와 일치하지 않는지 확인

if(menu.stream().noneMatch(Beverage::isTea)) {
	System.out.println("모든 음료수가 차가 아니다!!");
}

 

매칭 메소드들은 스트림 쇼트 서킷 기법을 사용한다. 즉, 자바의 &&, || 와 같은 연산을 사용한다.

쇼트 서킷 Short-Circuit
여러 개의 조건이 중첩된 상황에서, 값이 결정나면 이후의 불필요한 연산을 진행하지 않고 조건문을 빠져나가 실행 속도를 높이는 기법

 

(2) 검색

- findAny : 요소 검색

Optional<Beverage> beverage =
     menu.stream()
         .filter(Beverage::isTea)
         .findAny();

 

- findFirst : 첫번째 요소 찾기

Optional<Beverage> beverage =
     menu.stream()
         .filter(Beverage::isTea)
         .findFirst();
Optional<T>
값의 존재 여부나 부재 여부를 표현하는 컨테이너 클래스

findAny, findFirst 의 경우 아무것도 반환하지 않을 수 있다. 즉, null을 반환하게 되는데 이는 에러를 쉽게 일으키는 요인 중 하나다.

Optional을 쓰면 아래와 같이 값이 없을 때 어떻게 처리할 지 강제할 수 있다.

// isPresent 는 값이 존재하면 true, 없으면 false를 반환한다
boolean presnetBeverage = menu.stream()
                        .filter(Beverage::isTea)
                        .findFirst()
                        .isPresent();
// ifPresent는 값이 있으면 Consumer<T>를 수행한다
menu.stream()
    .filter(Beverage::isTea)
    .findFirst()
    .ifPresent(System.out.println(beverage.getName()));
// get은 값이 있으면 반환하고 없으면 NoSuchElementException을 반환한다.
Beverage beverage = menu.stream()
                        .filter(Beverage::isTea)
                        .findFirst()
                        .get();
// orElse는 값이 없으면 특정 값을 대신 반환하거나 특정 메소드를 수행할 수 있다
Beverage beverage = menu.stream()
                        .filter(Beverage::isTea)
                        .findFirst()
                        .orElse(new Beverage("우아한 아메리카노"));
// orElseThrow는 값이 없으면 특정 예외를 던진다
Beverage beverage = menu.stream()
                        .filter(Beverage::isTea)
                        .findFirst()
                        .orElseThrow(BeverageException::new)

 

5. reduce : 리듀싱 (폴드)

- 요소의 합

// sum 변수의 초기값을 0으로 한다
int sum = numbers.stream().reduce(0, (a, b) -> a + b );

// 초기값이 없으면?
Optiona<Integer> sum = numbers.stream.reduce(0. Integer::sum);

 

- 최댓값과 최솟값

Optional<Integer> max = numbers.stream().reduce(Integer::max);

Optional<Integer> min = numbers.stream().reduce(Integer::min);

 

 

6. 실전.. 대신 간단 퀴즈 

Q. Dish 객체는 int를 반환하는 getCalories 메소드를 가지고 있다. menu에 있는 Dish들의 칼로리 합을 구하는 옳은 스트림 구현을 모두 고르시오.

// 1번
int calories = menu.stream()
                   .map(Dish::getCalories)
                   .reduce(Integer::sum)
                   
// 2번
int calories = menu.stream()
                   .map(Dish::getCalories)
                   .sum()
                   
// 3번
int calories = menu.stream()
                   .map(Dish::getCalories)
                   .reduce(0, Integer::sum)
                                 
// 4번
Optional<Integer> calories = menu.stream()
                                 .map(Dish::getCalories)
                                 .reduce(0, Integer::sum)
                                 
// 5번
Optional<Integer> calories = menu.stream()
                                 .map(Dish::getCalories)
                                 .reduce(Integer::sum)

 

7. 숫자형 스트림

IntStream, DoubleStream, LongStream: 기본형 특화 스트림

 

매핑, 박싱 비용을 피할 수 있도록 각 타입 요소에 특화된 stream

 

- mapToInt : 숫자 스트림으로 매핑

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

- boxed : 객체 스트림으로 복원하기

List<Integer> calories = menu.stream()
                    .mapToInt(Dish::getCalories)
                    .boxed()
                    .collect(toList());

 

- range, rangeClosed : 숫자 범위

// 1과 100을 포함하지 않는다 -> 49개의 요소 반환
IntStream evens = IntStream.range(1,100)
                           .filter(n -> n % 2 == 0);


// 1과 100을 포함한다 -> 50개의 요소 반환                     
IntStream evens = IntStream.rangeClosed(1,100)
                           .filter(n -> n % 2 == 0);

 

8. 스트림 만들기

- Stream.of : 값으로 스트림 만들기

// 직접 값을 넣어 스트림 만들기
Stream<String> pairs = Stream.of("choonsik", "papi", "mark");

// 스트림 비우기
Stream<String> pairs = Stream.empty();

 

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

Stream<String> studyCrews =
    allCrews.filter(Crew::isStdyingJava)
            .map(Crew::getName)
            .flatMap(crew -> Stream.ofNullable(crew)));

 

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

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

 

- File.lines : 파일로 스트림 만들기

// 주어진 파일의 행 스트림을 문자열로 반환
Stream<String> lines =
     Files.line(Paths.get("data.txt"), Charset.forName("utf-8"));

(+) Files.list 는 디렉토리에서 목록을 읽어와 스트림으로 반환한다

String path = "D:/";
Stream<Path> dir = Files.list(Paths.get(path));

 

- Stream.iterate, Stream.generate : 함수로 무한 스트림(=언바운드 스트림) 만들기

Stream.iterate(0, n -> n + 2)
      .limit(10)                    // 보통 무한한 값을 출력하지 않도록 limit를 연결해 사용한다
      .forEach(System.out::println);
Stream.generate(Math::random)      // generate는 Supplier<T>를 인수로 받아 값을 생산한다
      .limit(5)
      .forEach(System.out::println);

 

 

참고

[ 모던 자바 인 액션 ] Ch5. 스트림 활용