반응형

 

이번 문제는 주어진 트리가 높이 균형 트리인지 판단하는 문제입니다.

왼쪽 노드와 오른쪽 노드의 depth 차이가 1이하여야만 합니다.

 

풀이로는 재귀를 통해 각 노드를 순회하면서 depth를 아래부터 1씩 증가시키면서 매깁니다.

depth를 매기면서 각 왼쪽과 오른쪽의 depth를 비교하여 1보다 크면 불균형으로 판단하도록 하였습니다.

 

풀이는 아래와 같습니다.

 

class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

class Solution:

    def isBalanced(self, root: TreeNode) -> bool:
        if not root: return True

        res = self.searchDepth(root)
        if res == -1: return False
        return True

    def searchDepth(self, root: TreeNode):
        if root == None: return 0

        right_depth = self.searchDepth(root.right)
        if right_depth == -1 : return -1
        
        left_depth = self.searchDepth(root.left)
        if left_depth == -1: return -1
        
        if abs(right_depth - left_depth) > 1: return -1
        
        return max(right_depth, left_depth) + 1
반응형
반응형

1. 서론

이번 포스팅에서는 Chapter6의 스트림으로 데이터 수집 에 대해 진행하도록 하겠습니다.

 

2. 컬렉터란 무엇인가?

Collector 는 java.util.stream 에 있는 인터페이스로, 리듀싱 연산을 어떻게 구현할지 제공하는 메서드 집합입니다.

앞장에서 많이 사용한 스트림 함수 중 collect는 이 Collector 인터페이스를 인수로 받습니다.

 

그럼 앞장에서 자주 인수로 전달한 Collectors는 Collector의 구현체일까요? 아쉽지만 아닙니다.

 

코드를 깊게 파보시면 바로 아시겠지만 Collector의 구현체는 CollectorImpl 입니다.

 

그럼 대체 Collectors는 왜 가능한걸까요?

 

코드를 자세히 보시면 Collectors는 일종의 유틸 클래스로 inner class로 CollectorImpl를 가지고 있습니다.

그리고 toList와 같은 정적 메서드가 호출될때마다 이 CollectorImpl를 만들어 반환하는 형태입니다.

 

 

1) 고급 리듀싱 기능을 수행하는 컬렉터

 

스트림 함수인 collect의 동작방식은 인자로 받은 Collector로 리듀싱 연산을 수행하여 결과를 반환합니다.

 

일반적으로 많이 사용하는 toList, toSet 등등을 모아둔 유틸 클래스가 Collectors로 보시면 됩니다.

 

일반적으로 Collectors는 앞절에서 살펴본 sum, max와 같은 연산이 아닌
요소를 어떤 자료구조에 담아 반환할지의 리듀싱 기능을 제공합니다.

책에서는 이를 고급 리듀싱기능으로 분류하였습니다.

 

2) 미리 정의된 컬렉터

 

Collectors는 정적 팩토리 메서드 패턴을 통해 자주 사용하는 CollectorImpl를 제공하고 있습니다.

책에서는 이를 미리 정의된 컬렉터라고 일컫습니다.

 

Collectors는 아래와 같이 크게 3가지를 제공합니다.

 

  • 스트림 요소를 하나의 값으로 리듀스하고 요약
  • 요소 그룹화
  • 요소 분할

3. 리듀싱과 요약

Collectors에서 제공하는 정적 메서드들은 결국 스트림의 최종 연산으로 취급이 되며, reduce 작업을 하여 만들어지게 됩니다.

 

Collectors에서 제공하는 reduce 역할을 수행하는 정적 메소드들에 대해 소개하겠습니다.

 

1) counting

 

이 메서드는 Collectors가 제공하는 컬렉터로 요소의 갯수를 세어 반환합니다.

 

아래는 예제입니다.

 

long howManyDishes = menu.stream().collect(Collectors.counting());

 

 

2) maxBy, minBy

 

maxBy, minByCollectors가 제공하는 컬렉터로 요소 중 최댓값, 최솟값을 찾을때 사용합니다.

최대인지 최소인지 구별하기 위해 인자로는 Comparator를 받습니다.

 

아래는 예제입니다.

 

Comparator<Dish> dishCaloriesComparator = Comparator.comparingInt(Dish::getCalories);
Optional<Dish> mostCalorieDish = menu.stream().collect(Collectors.maxBy(dishCaloriesComparator));

 

3) 요약 연산

 

Collectors는 합계, 평균을 위한 요약 연산도 제공한다.

 

메서드는 아래와 같습니다.

단, 인수는 언박싱인 int, long, double 타입으로 반환하는 함수형 인터페이스의 구현체 혹은 람다여야 합니다.

 

  • summingInt
  • summingDouble
  • summingLong
  • averagingInt
  • averagingDouble
  • averagingLong

사용 예제는 아래와 같습니다.

 

int totalCalories = menu.stream().collect(Collectors.summingInt(Dish::getCalories));

 

만약 한번의 collect API로 합계, 평균 등의 값을 한번에 수행해야 할 경우에는 아래와 같은 메서드를 사용하면 됩니다.

 

  • summarizingInt
  • summarizingDouble
  • summarizingLong

아래는 예제 및 결과입니다.

 

IntSummaryStatistics menuStatistics = menu.stream()
            .collect(Collectors.summarizingInt(Dish::getCalories));

System.out.println(menuStatistics);

/*
 * 결과
*/
IntSummaryStatistics{count=?, sum=?, average=?, min=?, max=?}

 

 

4) 문자열 연결

 

Collectors에서 제공하는 joining 메서드를 사용하면,

스트림에 각 요소에 toString 메서드를 호출하여 하나의 문자열로 연결하여 반환합니다.

 

추가로, joining 메서드는 문자열을 연결할 때 사용할 구분자를 인자로 받을 수있습니다.

 

아래는 예제입니다.

 

String shortMenu = menu.stream().map(Dish::getName).collect(Collectors.joining(", "));

// pork, beef, chicken, ...

 

5) 범용 리듀싱 요약 연산

 

지금까지 알아본 메서드는 사실 Collectors.reducing의 메서드를 이용하여 특화된 값을 도출하도록 만들은 것입니다.

 

이는 단순히, 개발 편의와 가독성을 위해 제공한 것입니다.

 

그럼, 특화된것이 아니라 커스텀을 해야한다면 스스로 reducing 메서드를 사용해야 합니다.

 

때문에 reducing 메서드에 대해 알아보겠습니다.

 

reducing 메서드는 인자로 3개를 받고 있습니다.

 

  • 첫번째 인자 = 리듀싱 연산의 초기값입니다.
  • 두번째 인자 = 각 요소에 적용할 변환 함수입니다.
  • 세번째 인자 = 같은 종류의 요소를 하나의 요소로 합칠 BinaryOperator입니다.
reducing 메서드는 인자를 한개만 받을 수도 있습니다.
이 경우는 첫번째 인자가 스트림 요소의 첫번째가 되고, 두번째 인자는 자신을 그대로 반환하는 Function.identity()가 됩니다.
단, 스트림이 비어있는 경우가 있기 때문에 결과값으로는 Oprional이 씌어지게 됩니다.

 

아래는 summingInt 를 reducing을 통해 구현한 예제입니다.

 

int totalCalories = menu.stream().collect(Collectors.reducing(
        0, // 초기값
        Dish::getCalories, // 변환 함수
        Integer::sum // 누적 함수
));

 

 

 

 

반응형

 

 

 

 

 

4. 그룹화

데이터 처리를 하다보면 그룹핑이 필요한 경우가 있습니다.

이를 위해 Collectors 는 groupingBy와 같은 그룹핑을 할수 있도록 제공합니다.

 

아래는 각 Dish의 타입별로 그룹핑하는 예제입니다.

 

Map<Dish.Type, List<Dish>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(Dish::getType));
        
// 결과
{
    FISH=[prawns, salmon], 
    OTHER=[french fries, rice, season fruit, pizza], 
    MEAT=[pork, beef, chicken]
}

 

Dish::getType과 같이 그룹화되는 기준을 제공하는 함수를 분류함수라고 합니다.

 

만약 분류함수가 복잡하다면 코드블록을 사용하여 커스텀하면 됩니다.

 

아래는 각 칼로리값에 따라 분류를 나누는 예제입니다.

 

public enum CaloricLevel {DIET, NORMAL, FAT}

Map<Dish.Type, List<Dish>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(
                dish -> {
                    if (dish.getCalories() <= 400) return CaloricLevel.DIET;
                    else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
                    else return CaloricLevel.FAT;
                }));

 

 

1) 그룹화된 요소 조작

 

그룹화 진행 후에 각 그룹화된 요소를 조작해야 하는 경우가 있습니다.

이를 위해 Collectors.groupingBy는 추가로 Collector 인자를 받을수 있습니다.

 

이는, 첫번째 인자를 통해 그룹핑이 진행 된 후, 두번째 인자인 Collector를 적용하게 됩니다.

 

Collectors는 또 미리 자주 사용하는 mapping 함수를 제공합니다.

java 9 부터는 Collectors 에 filtering, flatMapping이 추가되었습니다.
[java docs 9] 참조 

 

아래는 타입별로 그룹핑한 요리 리스트가 아닌 요리명 리스트로 바꾼 예제입니다.

 

Map<Dish.Type, List<String>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(
                Dish::getType,
                Collectors.mapping(Dish::getName, Collectors.toList())
                ));

 

2) 다수준 그룹화

 

위에서 설명했듯이 groupingBy는 두번째 인수로 Collecto를 받습니다.

 

그 말은, 두번째 인수로 또 groupingBy를 받을 수 있습니다.

 

결국, n차원의 그룹화가 가능하다는 말로 직결됩니다.

 

아래는 2 수준으로 그룹핑하는 예제입니다.

 

Map<Dish.Type, Map<CaloricLevel, List<Dish>>> dishesByType =
        menu.stream().collect(Collectors.groupingBy(
                Dish::getType,
                Collectors.groupingBy(
                        dish -> {
                            if (dish.getCalories() <= 400) {
                                return CaloricLevel.DIET;
                            } else if (dish.getCalories() <= 700) {
                                return CaloricLevel.NORMAL;
                            } else {
                                return CaloricLevel.FAT;
                            }
                        }
                )
        ));

// 결과
{
    FISH = {
        NORMAL=[salmon], 
        DIET=[prawns]
    }, 
    MEAT = {
        FAT=[pork], 
        NORMAL=[beef], 
        DIET=[chicken]
    }, 
    OTHER = {
        NORMAL=[french fries, pizza], 
        DIET=[rice, season fruit]
    }
}

 

3) 서브그룹으로 데이터 수집

 

groupingBy 의 두번째 인자로 Collector를 인자로 받는다는 것은 원하는 데이터 수집도 가능하다는 말이 됩니다.

 

대표적으로 아래와 같습니다.

 

  • counting = 그룹별 요소의 갯수를 수집
  • maxBy = 그룹별 주어진 기준의 최댓값을 가진 요소 수집
  • summingInt = 그룹별 합계 값 수집
maxBy의 경우 반환값이 optional로 감싸게 됩니다.
만약 empty가 없는 경우에는 Collectors.collectingAndThen 을 통해 Optional.get을 적용하여 감싸지 않은 형태로도 가능합니다.

 

5. 분할

스트림에서의 분할은 프레디케이트를 분류 함수로 사용하는 특수한 그룹화 기능으로 분할 함수로 일컫습니다.

 

프레디케이트가 분류함수이기 때문에 키는 Boolean 값으로 그룹핑은 최대 2개가 됩니다.

 

Collectors에서는 partitioningBy 메서드로 이를 제공합니다.

 

아래는 Collectors.partitioningBy 예제입니다.

 

Map<Boolean, List<Dish>> partitionedMenu = 
        menu.stream().collect(Collectors.partitioningBy(Dish::isVegetarian));

 

Collectors.partitioningBy 또한 groupingBy와 같이 두번째 인수로 Collector 를 받을 수 있어 다양한 커스텀 연산이 가능합니다.

 

6. Collector 인터페이스

Collector 인터페이스의 시그니처와 다섯개의 메서드 정의는 아래와 같습니다.

 

public interface Collector<T, A, R> {
    Supplier<A> supplier();
    BiConsumer<A, T> accumulator();
    BinaryOperator<A> combiner();
    Function<A, R> finisher();
    Set<Characteristics> characteristics();
}

 

각 제네릭은 아래와 같은 용도입니다.

  • T = 수집될 스트림 항목
  • A = 누적자로 수집 과정에서 중간 결과를 누적하는 객체 타입
  • R = 수집 연산의 결과 객체 타입 (대게 컬렉션 타입입니다.)

 

각 메서드의 용도는 아래와 같습니다.

 

1) supplier 메서드 : 새로운 결과 컨테이너 만들기

 

supplier 메서드는 빈 결과로 이루어진 Supplier를 반환해야 합니다.

즉, supplier는 수집 과정에서 빈 누적자 인스턴스를 만드는 파라미터 함수입니다.

 

toList와 같은 컬렉터는 ArrayList::new 가 supplier입니다.

 

2) accumulator 메서드 : 결과 컨테이너에 요소 추가하기

 

acuumulator 메서드는 리듀싱 연산을 수행하는 함수를 반환합니다.

 

함수의 반환값은 void입니다.

 

toList 컬렉터의 acuumulator는 List::add입니다.

 

3) finisher 메서드 : 최종 변환값을 결과 컨테이너로 적용하기

 

finisher 메서드는 스트림 탐색이 끝나고 누적자 객체를 최종 결과로 변환하면서 누적 과정을 끝낼 때 호출할 함수를 반환해야 합니다.

 

toList 컬렉터의 finisher는 누적자 객체가 이미 반환 형태이기 때문에 Function.identity() 입니다.

 

사실상, toList는 위 3가지로도 기능 구현이 가능합니다.
다만, 병렬성을 통한 최적화 판단 및 구현이 있어 내부적으로는 더욱 복잡하며
이를 위해 combiner 메서드와 characteristics 가 있습니다.

 

4) combiner 메서드 : 두 결과 컨테이너 병합

 

combiner는 스트림의 서로 다른 서브 파트를 병렬로 처리할때 누적자가 이 결과를 어떻게 처리할지 정의합니다.

 

toList 컬렉터에서는 비교적 간단합니다.

왜냐하면, 2개의 리스트를 이으기만 하면 되기 때문입니다.

 

아래는 toList 컬렉터의 combiner 입니다.

 

(left, right) -> { left.addAll(right); return left;

 

이 메서드를 통해 병렬로 수행이 가능하게 됩니다.

 

아래는 병렬화 리듀싱 과정을 간단하게 보여주는 그림입니다.

 

 

5) Characteristics 메서드

 

characteristics 메서드는 컬렉터의 연산을 정의하는 Characteristics 형식의 불변 집합을 반환합니다.

 

characteristics는 스트림을 병렬로 리듀스 할 것인지, 병렬로 한다면 어떤 최적화를 선택해야 하는지 힌트를 제공합니다.

 

Characteristics는 enum 클래스로 아래 3개의 값을 가지고 있습니다.

 

  • CONCURRENT = 다중 스레드에서 accumulator 함수를 동시에 호출할 수 잇으며 이 컬렉터는 병렬 리듀싱이 가능함.
    • 컬렉터 플래그에 UNORDERED를 함께 설정하지 않은 경우, 데이터 소스가 정렬되어 있지 않은 상황에서만 병렬 리듀싱 가능
  • UNORDERED = 리듀싱 결과는 스트림 요소의 방문 순서나 누적순서에 영향을 받지 않음.
  • IDENTITY_FINISH = finisher 메서드가 반환하는 함수는 단순히 identify 이므로 생략 가능.

toList의 경우에는 IDENTITY_FINISH 입니다.

 

그 이유는 accumulator로 List::add를 사용하는데 병렬로 처리 시 순서가 엉킬수 있기 때문입니다.

그 말은, 누적 순서에도 영향이 있다는 말이 됨으로 CONCURRENT와 UNORDERED는 해당 사항이 되지 않습니다.

 

7. 마무리

 

이번 포스팅에서는 Chapter 6인 스트림으로 데이터 수집에 대해 진행하였습니다.

다음에는 Chapter 7인 병렬 데이터 처리와 성능에 대해 포스팅하겠습니다.

반응형

'Programming > ModernJavaInAction' 카테고리의 다른 글

(8) 컬렉션 API 개선  (0) 2020.04.13
(7) 병렬 데이터 처리와 성능  (0) 2020.04.11
(5) 스트림 활용  (0) 2020.04.04
(4) 스트림 소개  (0) 2020.03.28
(3) 람다 표현식  (0) 2020.03.28
반응형

1. 서론

이번 포스팅에서는 Chapter5의 스트림 활용 에 대해 진행하도록 하겠습니다.

 

2. 필터링

java 8에서는 스트림의 요소를 선택하는 필터링 기능을 제공합니다.

 

소개할 기능으로는 filter와 distinct 입니다.

 

1) 프레디케이트로 필터링

 

스트림 인터페이스는 filter 메서드를 지원하고 있습니다.

이 메서드는 인자로 프레디케이트를 받고, 프레디케이트와 일치하는 요소를 포함하는 스트림을 반환합니다.

 

아래는 예제입니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
    private final String name;
    private final boolean vegetarian;
    private final int calories;
    private final Type type;

    enum Type {
        MEAT, FISH, OTHER
    }
}

public static void main(String[] args) {
    List<Dish> menu = Arrays.asList(
            new Dish("pork", false, 800, Dish.Type.MEAT),
            new Dish("beef", false, 700, Dish.Type.MEAT),
            new Dish("chicken", false, 400, Dish.Type.MEAT),
            new Dish("french fries", true, 530, Dish.Type.OTHER),
            new Dish("rice", true, 350, Dish.Type.OTHER),
            new Dish("season fruit", true, 120, Dish.Type.OTHER),
            new Dish("pizza", true, 550, Dish.Type.OTHER),
            new Dish("prawns", false, 300, Dish.Type.FISH),
            new Dish("salmon", false, 450, Dish.Type.FISH)
    );

    List<Dish> vegetarianMenu = menu.stream()
            .filter(Dish::isVegetarian)
            .collect(Collectors.toList());
}

 

2) 고유 요소로 필터링

 

스트림에서는 distinct 메서드를 지원합니다.

 

이 메서드는 고유한 요소들로 이루어진 스트림을 반환합니다.

고유를 판단하는 기준은 java의 hashCode, equals로 결정됩니다.

 

아래는 예제입니다.

 

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
        .filter(i-> i %2 == 0)
        .distinct()
        .forEach(System.out::println);

 

3. 스트림 슬라이싱

스트림에서는 특정 요소를 선택하거나 스킵하는 방법을 제공하며 이를 슬라이싱이라고 일컫습니다.

 

1) 프레디케이트를 이용한 슬라이싱

 

자바 9에서는 스트림의 필터측면으로 특화된 takeWhile, dropWhile를 제공하고 있습니다.

 

takeWhile는 filter와 동일하게 작동하는것 같지만, 처음으로 프레디케이트가 거짓이 되면 종료하게 됩니다.

 

예제는 아래와 같습니다.

 

List<Dish> specialMenu = Arrays.asList(
        new Dish("season fruit", true, 120, Dish.Type.OTHER),
        new Dish("prawns", false, 300, Dish.Type.FISH),
        new Dish("rice", true, 350, Dish.Type.OTHER),
        new Dish("chicken", false, 400, Dish.Type.MEAT),
        new Dish("french fries", true, 530, Dish.Type.OTHER)
);

List<Dish> sliceMenu1 =
        specialMenu.stream()
        .takeWhile(dish -> dish.getCalories() < 320)
        .collect(Collectors.toList());

 

위 예제는 칼로리가 320보다 큰 rice를 만나는 시점까지만을 스트림으로 반환하게 됩니다.

filter를 통해 모든 요소에 대해 프레디케이트를 적용하게되어 성능에 큰 이슈가 있을 시 사용하면 됩니다.
단, 위와 같이 sort가 되어 있는것처럼 데이터의 상태를 자세히 보고 사용해야 합니다.

 

dropWhile 은 takeWhile과 반대로 프레디케이트가 거짓이 되는 지점까지 발견된 요소를 버립니다.

 

예제는 아래와 같습니다.

 

List<Dish> specialMenu = Arrays.asList(
        new Dish("season fruit", true, 120, Dish.Type.OTHER),
        new Dish("prawns", false, 300, Dish.Type.FISH),
        new Dish("rice", true, 350, Dish.Type.OTHER),
        new Dish("chicken", false, 400, Dish.Type.MEAT),
        new Dish("french fries", true, 530, Dish.Type.OTHER)
);

List<Dish> sliceMenu2 =
        specialMenu.stream()
        .dropWhile(dish -> dish.getCalories() < 320)
        .collect(Collectors.toList());

 

이번에는 거짓이 되는 rice 이전 요소는 모두 버리게 됩니다. 

 

 

2) 스트림 축소

 

스트림은 특정 요소갯수만을 가진 스트림을 반환하는 limit 메서드를 제공합니다.

ansiansi sql의 limit과 같이 생각하면 됩니다.

 

예제는 아래와 같습니다.

 

List<Dish> dishes = specialMenu.stream()
        .filter(dish -> dish.getCalories() > 300)
        .limit(3)
        .collect(Collectors.toList());

 

3) 요소 건너뛰기

 

스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip 메서드를 제공합니다.

 

예제는 아래와 같습니다.

 

List<Dish> dishes = specialMenu.stream()
	    .filter(dish -> dish.getCalories() > 300)
	    .skip(2)
	    .collect(Collectors.toList());

 

4. 매핑

스트림에서는 자바 객체 타입을 변경하는 스트림을 반환하는 map, flatMap을 제공합니다.

 

1) 스트림의 각 요소에 함수 적용하기

 

스트림에 들어있는 모든 요소에 함수를 적용하고 싶다면 map을 사용하면 됩니다.

foreach도 가능하지만 foreach의 경우에는 반환값이 void인것을 염두하여 사용해야 합니다.
반면 map의 경우에는 스트림을 반환하기 때문에 다른 스트림 메서드를 붙일 수 있습니다.

 

예제는 아래와 같습니다.

 

List<Integer> dishNameLengths = menu.stream()
        .map(Dish::getName)
        .map(String::length)
        .collect(Collectors.toList());

 

2) 스트림 평면화

 

스트림 처리를 하다보면 2개이상의 스트림을 평면화된 스트림으로 만들어야 하는 경우가 있습니다.

 

이를 제공하는것이 flatMap입니다.

 

책에서는 {"Hello", "World"}와 같은 문자열 배열에서 고유문자로 이루어진 {"H", "e", "l", "o", "W", "r", "d"} 인 스트림을 만드는 예제가 있습니다.

 

처음으로 단순히 앞에서 배운 map, distinct를 사용하는 케이스를 생각할 수 있습니다.

 

코드는 아래와 같습니다.

 

List<String> words = Arrays.asList("Hello", "World");
words.stream()
        .map(word -> word.split(""))
        .distinct()
        .collect(Collectors.toList());

 

하지만 원하는 결과는 나오지 않게 됩니다.

이유로는 map으로 반환되는 스트림의 객체 타입은 String[]이지 String이 아니기 때문에 distinct가 제대로 동작하지 않기 때문입니다.

 

아래는 flatMap를 사용한 코드입니다.

 

List<String> words = Arrays.asList("Hello", "World");
words.stream()
        .map(word -> word.split(""))
        .flatMap(Arrays::stream)
        .distinct()
        .collect(Collectors.toList());

 

이번에는 flatMap을 통해 Stream<String[]>을 Stream<String>으로 스트림 안에 있는

String 배열을 평면화시킨 스트림으로 변경한 후 distinct 메소드를 수행하도록 하였습니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

5. 검색과 매칭

스트림 API에서는 스트림에 특정 요소가 있는지 검색하는 기능도 제공하고 있습니다.

 

대표적으로는 아래와 같습니다.

 

  • allMatch
  • anyMatch
  • noneMatch
  • findFirst
  • findAny

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

 

주어진 스트림에서 적어도 한개 요소가 프레디케이트에서 참인지 확인하는 메서드로 anyMatch를 제공합니다.

확인 용도이기 때문에 반환값은 boolean 입니다.

 

예제는 아래와 같습니다.

 

if(menu.stream().anyMatch(Dish::isVegetarian)) {
    ...
}

 

2) 프레디케이트가 모든 요소와 일치하는지 검사

 

anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트에서 참인지 확인하는 allMatch가 있습니다.

 

예제는 아래와 같습니다.

 

boolean isHealthy = menu.stream().allMatch(dish -> dish.getCalories() < 1000);

 

3) NONEMATCH

 

allMatch와 반대로 스트림의 요소가 모두 프레디케이트의 거짓인지 확인하는 noneMatch 메서드가 있습니다.

 

예제는 아래와 같습니다.

 

boolean isHealthy = menu.stream().noneMatch(dish -> dish.getCalories() >= 1000);

 

4) 요소 검색

 

스트림에서 확인용 혹은 샘플링을 위해 임의의 한 요소만을 필요로 할때가 있습니다.

 

이를 위해 findAny 메서드를 지원하고 있습니다.

 

이 메서드는 주어진 스트림에서 임의로 한개를 반환합니다.

 

findAny는 주어진 스트림에 요소가 한개도 없을 수 있기 때문에 Optional로 감싸 반환합니다.

 

예제는 아래와 같습니다.

 

Optional<Dish> dish = menu.stream()
        .filter(Dish::isVegetarian)
        .findAny();

 

5) 첫 번째 요소 찾기

 

 findAny는 임의로 한개를 반환하지만 첫번째 요소가 필요한 경우가 있습니다.

이를 위해 findFirst 메서드를 지원하고 있습니다.

 

이는 스트림의 첫번째 요소를 반환하며 findAny와 같이 Optional로 감싼 반환값을 반환합니다.

 

예제는 아래와 같습니다.

 

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree = someNumbers.stream()
        .map(n -> n * n)
        .filter(n -> n % 3 == 0)
        .findFirst();

 

6. 리듀싱

스트림에서는 모든 요소를 통해 값으로 도출하는 연산인 리듀싱을 지원합니다.

 

대표적으로 아래와 같은 일들을 할 수 있습니다.

 

  • 요소의 합/ 곱
  • 최댓값/ 최솟값

 

1) 요소의 합/ 곱

 

스트림 요소의 합 혹은 곱을 구할때는 아래와 같이 할 수 있습니다.

 

int sum = numbers.stream().reduce(0, (a, b) -> a + b);
int sum2 = numbers.stream().reduce(0, Integer::sum);
int multiple = numbers.stream().reduce(1, (a, b) -> a * b);

 

 

위에는 스트림의 합과 곱을 구하는 reduce 연산입니다.

reduce 연산의 경우 첫번째 인자로 초기값을 설정 가능합니다.

초깃값의 경우 생략이 가능합니다 .
단, 생략하는 경우 빈 스트림의 경우가 있기 때문에 반환값은 Optional을 감싼 형태가 됩니다.

 

두번째 인자로는 두 요소를 조합해 새로운 값을 만드는 BinaryOperator를 받습니다.

이 두 요소의 첫번째는 이전 BinaryOperator로 나온 결과값이며 두번째는 스트림의 값입니다.

 

결국, reduce는 점진적으로 스트림의 값에 BinaryOperator를 적용하여 값을 한개로 만들어 반환합니다.

 

2) 최댓값/ 최솟값

 

위와 같은 맥락으로 스트림 요소중에 최댓값과 최솟값도 도출할 수 있습니다.

 

코드는 아래와 같습니다.

 

int max = numbers.stream().reduce(0, Integer::max);
int min = numbers.stream().reduce(0, Integer::min);

 

 

7. 상태가 있는 스트림, 없는 스트림

스트림 연산을 보다보면 map, filter와 같이 이전 스트림의 값을 저장해야할 필요가 없는 메서드가 있습니다.

이를, 상태를 갖지 않는 스트림 연산이라고 일컫습니다.

 

하지만, reduce, sum, max, sorted, distinct와 같이 이전 스트림의 값을 알아야만 처리가 가능한 메서드가 있습니다.

이는, 상태를 갖는 스트림 연산이라고 일컫습니다.

 

이 상태를 갖는 연산의 경우에는 내부 버퍼에 값을 저장하고 사용하게 됩니다.

 

8. 숫자형 스트림

스트림은 모두 제네릭 타입을 가지고 있습니다.

제네릭에는 기본적으로 박싱타입만을 선언해야 합니다.

 

그로인해, 스트림 연산중에는 박싱 -> 언박싱의 오버헤드가 발생하게 됩니다.

스트림에서는 이를 위해 기본형 특화 스트림을 제공하고 있습니다.

 

1) 숫자 스트림으로 매핑

 

숫자 특화 스트림으로 변환시에는 mapToInt, mapToDouble, mapToLong 을 사용하면 됩니다.

3개 메서드는 각각 IntStream, DoubleStream, LongStream을 반환합니다.

 

위와 같은 메서드를 통해 아래와 같이 숫자에 특화된 스트림 메서드가 바로 가능하다는 점입니다.

  • sum
  • max
  • min
  • average

아래는 예제 코드입니다.

 

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

 

2) 객체 스트림 복원

 

스트림은 반대로 기본 특화 스트림에서 박싱형 기본 스트림으로도 변경하는 boxed 메서드를 제공합니다.

 

예제는 아래와 같습니다.

 

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

 

3) 특화형 Optional

 

스트림과 같이 Optional도 기본 특화형이 있습니다.

 

대표적으로 OptionalInt, OptionalDouble, OptionalLong 이 있습니다.

 

사용 예제는 아래와 같습니다.

 

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

 

4) 숫자 범위

 

개발을 하다보면 특정 범위의 숫자를 이용해야 하는경우가 있습니다.

대표적으로 for(int i=0; i< 100; i++) 와 같은 연산을 들 수 있습니다.

 

스트림에서도 이를 위해, range와 rangeClosed 함수를 제공합니다.

 

두 함수는 IntStream, LongStream에서 사용가능합니다.

 

두 함수 모두 2개의 인자를 받으며 첫번째 인자는 초기값, 두번째 인자는 종료값을 의미합니다.

 

차이 점으로는 range는 시작값과 종료값은 결과에 포함되지 않고, rangeClosed는 초기값, 종료값이 포함된다는 점입니다.

 

예제는 아래와 같습니다.

 

IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
System.out.println(evenNumbers.count());

 

9. 스트림 만들기

스트림은 위와 같이 컬렉션에서 생성이 가능합니다.

 

하지만, 컬렉션 이외에도 일련의 값, 배열, 파일 등으로도 생성이 가능합니다.

 

이제 스트림 만드는 각 종류를 소개합니다.

 

1) 값으로 스트림 만들기

 

Stream.of를 통해 임의의 값으로 스트림을 만들 수 있습니다.

 

예제는 아래와 같습니다.

 

Stream<String> stream = Stream.of("Modern ", "Java ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

 

요소가 없는 빈 스트림을 만들자 할때는 Stream.empty를 사용하시면 됩니다.

 

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

 

2) null이 될 수 있는 객체로 스트림 만들기

 

개발을 하다보면 null 값도 스트림으로 만들어야 하는 경우가 생깁니다.

이를 위해, 자바 9에서는 ofNullable 메서드를 제공합니다.

 

예제는 아래와 같습니다.

 

Stream<String> homeValueStream = Stream.ofNullable(System.getProperty("home"));

 

3) 배열로 스트림 만들기

 

컬렉션이 아닌 배열에서도 Arrays.stream을 통해 스트림을 만들 수 있습니다.

 

예제는 아래와 같습니다.

 

int[] numbers  = {1, 2, 3, 4 ,5};
int sum = Arrays.stream(numbers).sum();

 

4) 파일로 스트림 만들기

 

파일 처리의 I/O 연산을 위해 사용하는 java.nio.file 에서도 스트림 처리가 가능하도록 업데이트 되었습니다.

 

예제는 아래와 같습니다.

 

long uniqueWords = 0;
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) {
    e.printStackTrace();
}

 

파일의 고유 단어들이 몇개인지 찾는 예제입니다.

 

5) 함수로 무한 스트림 만들기

 

스트림 API에서는 무한 스트림을 만들 수 있는 Stream.iterate와 Stream.generate를 제공합니다.

 

무한 스트림을 만들수 는 있지만, 보통 limit을 통해 무한으로 사용하는 것을 방지합니다.

 

1. Stream.iterate

 

iterate 정적 메서드는 초깃값과 람다를 인수로 받아 새로운 값을 무한으로 생성할 수 있습니다.

이러한 스트림을 언바운드 스트림이라고 일컫습니다.

 

아래는 예제 코드입니다.

 

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

 

자바 9에서는 iterate 함수에 프레디케이트를 지원합니다.

 

아래는 0부터 4씩 증가하며 100보다 작은 값으로 스트림을 생성하는 예제입니다.

 

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

 

두번째 인자로 프레디케이트가 들어간것을 볼 수 있습니다.

 

2. Stream.generate

 

generate는 iterate와 같이 무한 스트림을 생성합니다.

 

차이점으로는 인수로 Supplier를 받는 다는 점입니다.

 

결국, generate는 무한히 지정한 Supplier를 호출하여 반환된 값으로 스트림을 생성하게 됩니다.

 

이는 iterate와 큰 차이를 가져오게 됩니다.

이유는 바로 인자로 Supplier를 받기 때문입니다.

 

Supplier는 별도로 생성하여 인자로 줄 수 있기 때문에 커스텀이 가능하게 됩니다.

그 말은, iterate와 같이 스트림으로 만드는 요소의 값이 불변이 아니라 가변이 될 수 있게 되는 것입니다.

 

아래는 그 예제 코드입니다.

 

IntSupplier fib = new IntSupplier() {
    private int previous = 0;
    private int current = 1;
    @Override
    public int getAsInt() {
        int oldPrevious = this.previous;
        int nextValue = this.previous + this.current;
        this.previous = this.current;
        this.current = nextValue;
        return oldPrevious;
    }
};

IntStream.generate(fib).limit(10).forEach(System.out::println);

 

10. 마무리

이번 포스팅에서는 Chapter 5인 스트림 활용에 대해 진행하였습니다.

다음에는 Chapter 6 스트림으로 데이터 수집에 대해 포스팅하겠습니다.

반응형

'Programming > ModernJavaInAction' 카테고리의 다른 글

(7) 병렬 데이터 처리와 성능  (0) 2020.04.11
(6) 스트림으로 데이터 수집  (0) 2020.04.04
(4) 스트림 소개  (0) 2020.03.28
(3) 람다 표현식  (0) 2020.03.28
(2) 동작 파라미터화 코드 전달하기  (0) 2020.03.21

+ Recent posts