Java

Java Stream (4) 스트림 주요 메소드

감동이중요해 2020. 7. 17. 15:12

스트림 주요 메소드

이번 포스팅에서는 스트림 활용 시 주로 사용되는 메소드를 다룬다.

지난 포스팅과 비슷한 예제로 진행하였다.

@Getter
@AllArgsConstructor
@EqualsAndHashCode    // equals, hashcode 자동 생성
public class City {
    private String name;
    private double area;
    private int population;
    private String areaCode;
}

List<City> cities = Arrays.asList(
        new City("Seoul", 605.2, 9720846, "02"),
        new City("Incheon", 1063.3, 2947217, "032"),
        new City("Ulsan", 1062, 1142190, "052"),
        new City("Daegu", 883.5, 2427954, "053"),
        new City("Gwangju", 501.1, 1455048, "062"),
        new City("Busan", 770.1, 3404423, "051")
);

데이터 선별

조건에 따라 데이터를 선별하는 중간 연산이다.

cities.add(new City("Seoul", 0, 0, "02"));

List<City> streamNameList = cities.stream()
        .filter(city -> city.getArea() > 800) // ← 데이터 필터링
        .distinct()                           // ← 중복 제거
        .collect(Collectors.toList());

Stream::filterPredicate 를 인자로 받는다.

중복 요소를 제거하고 싶다면 Stream::distinct 를 사용한다. (Seoul 하나가 삭제된다)

중복을 판단하는 조건은 City 클래스에 오버라이드한 equals / hashcode이다.

데이터 개수 조절

스트림 데이터를 자르거나 특정 요소만 선택한다.

  1. 개수 제한

    • Stream::limit 로 개수를 조절할 수 있다.

        List<String> streamNameList = cities.stream()
                .filter(city -> city.getArea() > 800)
                .sorted(Comparator.comparing(City::getArea))
                .map(City::getName)
                .limit(2) // ← 최대 두 개의 원소만 반환한다.
                .collect(Collectors.toList());
  2. 건너뛰기

    • Stream::skip 으로 처음 n개를 건너뛸 수 있다.

        cities.stream()
                .filter(city -> city.getArea() > 800)
                .sorted(Comparator.comparing(City::getArea))
                .map(City::getName)
                .skip(1) // ← 첫 번째 원소는 무시한다.
                .forEach(System.out::println);
      
        // 결과 (첫 번째 데이터 Daegu는 스킵)
        Ulsan
        Incheon
  3. 특정 조건에 부합하는 데이터만 선택

    • 전제조건

      • Java 9 이상에서 지원된다.

      • 데이터는 반드시 사용할 기준을 조건으로 정렬되어 있는 상태여야 한다.

          // 예시 데이터를 area 오름차순으로 재가공
          List<City> orderByAreaList = cities.stream()
                  .sorted(Comparator.comparing(City::getArea))
                  .collect(Collectors.toList());
    1. Stream::takeWhile

      • 스트림을 순회하다 조건이 false가 되는 순간 남은 데이터를 버린다.

          orderByAreaList.stream()
                  .takeWhile(city -> city.getArea() > 700)
                  .map(City::getName)
                  .forEach(System.out::println);
        
          // 결과
          Gwangju
          Seoul
        • 데이터는 광주, 서울, 부산, 대구, 인천 순으로 정렬되어 있으나, 조건에 부합하지 않는 데이터인 부산부터 모든 데이터를 제거한다.
    2. Stream::dropWhile

      • 스트림을 순회하다 조건이 true가 되는 순간 지금까지 순회한 데이터를 모두 버린다.

          orderByAreaList.stream()
                  .dropWhile(city -> city.getArea() < 700)
                  .map(City::getName)
                  .forEach(System.out::println);
        
          // 결과
          Busan
          Daegu
          Ulsan
          Incheon
    3. Stream::filter 와의 차이점

      • Stream::filter 는 모든 원소를 검사하여 조건에 맞는 데이터만 선택하나, takeWhile, dropWhile은 데이터를 버리는 방식이다.

데이터 변환

City 는 도시명, 면적, 인구수, 지역번호 데이터를 갖는 클래스이다.

사용자의 목적에 따라 도시명 데이터만 사용해야 할 수도 있을 것이다.

Stream::map 은 스트림 원소를 변환한다.

List<String> filteringList = cities.stream()
        .filter(city -> city.getArea() > 800)
        .sorted(Comparator.comparing(City::getArea))
        .map(city -> city.getName()) // City 클래스에서 도시명만 사용하도록 한다.
        .collect(Collectors.toList());

// 결과 (첫 번째 데이터 Daegu는 스킵)
Ulsan
Incheon

단일 스트림으로 변환

만약 cities에서 지역 번호에 사용된 숫자에서 중복을 제거한 목록를 스트림으로 얻어내고 싶다면 어떻게 해야 할까?

먼저 areaCode의 목록을 추출해본다.

List<String> areaCodes = cities.stream()
        .map(City::getAreaCode)
        .collect(Collectors.toList());
// ["02", "032", "052", "053", "062", "051"]
  1. areaCodes를 문자 단위로 분해

     0, 2
     0, 3, 2
     0, 5, 2
     0, 5, 3
     0, 6, 2
     0, 5, 1
  2. areaCodes를 단일 스트림으로 변환

     0, 2, 0, 3, 2, 0, 5, 2, 0, 5, 3, 0, 6, 2, 0, 5, 1

areaCodes를 스트림으로 추출하면 위의 1번처럼 문자열 하나가 데이터 하나로 변환된다.

문자 단위로 분해하여 2와 같은 단일 문자열 스트림을 반환해야 한다.

이럴 때 사용하는 것이 Stream::flatMap 이다.

areaCodes.stream()
        .map(areaCode -> areaCode.split("")) // areaCode를 문자 단위로 분해한 배열로 변환
        .flatMap(Arrays::stream)             // 변환된 스트림을 단일 스트림으로 변환
        .distinct()                          // 중복 제거
        .forEach(System.out::println);

// 결과
0
2
3
5
6
1

데이터 일치 여부

여기서 소개하는 메소드는 모두 boolean을 반환하는 최종 연산이다.

Short circuit evaluation 기법이 적용되어 값을 즉시 반환할 수 있는 상태가 되면 남은 원소를 확인하지 않는다.

  • Short circuit 예시

      if (true || false) { ... }
      if (false && true) { ... }
    • 두 식 모두 첫 번째 boolean만 평가하면 결과를 도출할 수 있다.
    • 따라서 두 번째 boolean은 확인하지 않는다.
  1. Stream::anyMatch

    • 검색 조건에 맞는 원소가 1개 이상인 경우 true

      boolean areaOver1000 = cities.stream()
            .anyMatch(city -> city.getArea() > 1000);
      
      boolean areaOver2000 = cities.stream()
            .anyMatch(city -> city.getArea() > 2000);
      
      System.out.println(areaOver1000);
      System.out.println(areaOver2000);
      
      // 결과
      true
      false
  2. Stream::allMatch

    • 모든 원소가 검색 조건에 일치해야 true

      boolean allAreasOver100 = cities.stream()
            .allMatch(city -> city.getArea() > 100);
      
      boolean allAreasOver1000 = cities.stream()
            .allMatch(city -> city.getArea() > 1000);
      
      System.out.println(allAreasOver100);
      System.out.println(allAreasOver1000);
      
      // 결과
      true
      false
  3. Stream::nonMatch

    • 모든 원소가 검색 조건에 일치하지 않아야 true (allMatch와 반대 동작)

      boolean allAreasAreNotOver1000 = cities.stream()
            .noneMatch(city -> city.getArea() > 1000);
      
      boolean allAreasAreNotOver2000 = cities.stream()
            .noneMatch(city -> city.getArea() > 2000);
      
      System.out.println(allAreasAreNotOver1000);
      System.out.println(allAreasAreNotOver2000);
      
      // 결과
      false
      true

데이터 검색

검색 메소드를 사용하면 스트림 파이프라인에서 적절한 데이터를 찾는다.

또한 데이터를 찾는 순간 검색이 종료되는 Short-circuit 기법이 적용된다.

검색 조건에 부합하는 데이터가 없을 수도 있다. 따라서 반환 시엔 Optional 을 사용한다.

  1. Stream::findAny

    • 검색 조건에 부합하는 임의의 데이터를 반환한다.

    • 순서에 구애받지 않기 때문에 병렬 스트림을 생성하면 결과가 다르게 나올 수 있다.

      Optional<City> find = cities.stream()
            .filter(city -> city.getArea() > 500) // 검색 조건은 filter를 활용한다.
            .findAny();
      
      System.out.println(find.get().getName());
  2. Stream::findFirst

    • 검색 조건에 부합하는 첫 번째 데이터를 반환한다.

      Optional<City> find = cities.stream()
            .filter(city -> city.getArea() > 500)
            .findFirst();
      
      System.out.println(find.get().getName());

데이터 연산

지금까지는 스트림 원소에 독립적인 로직을 적용하였다.

임의의 숫자가 담긴 배열의 총 합을 스트림을 활용하여 구해보자.

  • 데이터

      Random random = new Random();
    
      int[] array = new int[100];
      for (int i = 0; i < 100; i++) {
          array[i] = random.nextInt(100);
      }

for loop 활용

int sum = 0;
for (int num : array) {
    sum += num;
}
System.out.println(sum);

Stream 활용

Stream::reduce 메소드는 값을 연쇄적으로 계산할 때 사용한다.

reduce는 (int), IntBinaryOperator 를 매개변수로 받는다.

첫 번째 int는 초기값으로, 생략이 가능하다.

IntBinaryOperator 는 두 개의 int형 매개변수를 받아 int를 반환하는 함수형 인터페이스이다.

int sum = Arrays.stream(array)
        .reduce(0, Integer::sum);

System.out.println(sum);

최종 연산

최종 연산은 자료형, 컬렉션, void를 반환한다.

  1. Collection 반환

    1. List

      • 예제에서도 계속 사용했던 형태이다.

        List<String> streamNameList = cities.stream()
              .filter(city -> city.getArea() > 800)
              .sorted(Comparator.comparing(City::getArea))
              .map(City::getName)
              .collect(Collectors.toList());
    2. Map

      • 공통 요소를 그룹핑한다는 개념으로 Map을 생성한다.

        Map<String, List<City>> cityMap = cities.stream()
              .filter(city -> city.getArea() > 800)
              .collect(Collectors.groupingBy(City::getName));
    3. Set

      • 원본 List 에서 조건에 맞는 데이터만 선별하여 Set 으로 다시 저장한다.

        Set<City> citySet = cities.stream()
              .filter(city -> city.getArea() > 800)
              .collect(Collectors.toSet());
  2. 자료형 반환

    • 스트림의 원소 개수를 세는 예제

        System.out.println(cities.stream().count());
      
        // 결과
        6
  3. void

    • 순회

        cities.stream().map(City::getName).forEach(System.out::println);
      
        // 결과
        Seoul
        Incheon
        Ulsan
        Daegu
        Gwangju
        Busan