java

자바 8 - 스트림 2

Feb 09, 2020

리덕션, 옵션 타입

목차

1. 단순 리덕션
2. 옵션 타입
2.1 옵션 값을 사용하는 방법
2.2 옵션 값을 사용하지 않는 방법
2.3 옵션 값 생성하기
2.4 flatMap으로 옵션 값 함수 합성하기



1. 단순 리덕션

이전까지 스트림을 생성하고 변환하는 법을 알아 봤다면 여기서는 가장 중요한 부분인 스트림 데이터로부터 결과를 얻는 것이다. 여기서 다루는 메서드를 리덕션(reduction) 메서드라고 한다. 리덕션은 종료 연산(terminal operation)이다.

리덕션 메서드는 스트림을 넌스트림(nonstream) 값으로 리듀스(reduce)한다.

앞에서 살펴 본 count 메서드가 단순한 리덕션 메서드이다. 다른 단순 리덕션 메서드로는 min과 max가 있다. 주의할 점은 이 메서드들은 Optional<T> 값으로 반환 한다는 것이다. Optional<T> 값은 결과를 감싸고 있거나 결과가 없음을 나타낸다. 이유는 스트림이 비어있을 수 있기 때문이다. null을 반환하면 널 포인터 에러가 발생할 수 있다. 다음은 스트림의 최댓값을 얻는 방법이다.

String[] words = { "show", "me", "the", "money" };
Optional<String> largest = Stream.of(words).max(String::compareToIgnoreCase);
System.out.println("largest: " + largest.orElse(""));

// largetst: the

findFirst는 비어 있지 않은 컬렉션의 첫 번째 값을 반환한다. 이 메서드는 filter와 결합하면 유용하다. 예를 들어 ‘s’로 시작하는 단어가 있다면 글자 ‘s’로 시작하는 첫 번째 단어를 찾는다.

String[] words = { "sara", "supreme", "money", "show" };
Optional<String> startWithS = Stream.of(words).filter( s -> s.startsWith("s")).findFirst();
System.out.println(startWithS.orElse(""));
// sara

첫 번째 값은 물론 어떤 일치된 결과든 괜찮을 때는 findAny 메서드를 사용한다. 이 메서드는 스트림을 병렬화할 때 효과적이다.

String[] words = { "sara", "supreme", "money", "show" };
Optional<String> startWithS = Stream.of(words).parallel().filter( s -> s.startsWith("s")).findAny();
System.out.println(startWithS.orElse(""));
// sara, supreme, show 중 하나가 출력 됨

단순히 일치 요소가 알고 싶을 때는 anyMatch를 사용한다. 이 메서드는 predicate 인자를 받으므로 filter를 사용할 필요가 있다.

boolean aWordStartWithS = Stream.of(words).parallel().anyMatch(s -> s.startsWith("s"));
System.out.println("s로 시작하나요? " + aWordStartWithS);
// s로 시작하나요? true

모든 요소가 predicate와 일치하면 true를 반환하는 allMatch, 아무것도 일치하지 않으면 true를 반환하는 noneMatch 메서드로 있다.

단순 리덕션 메서드 정리



2. 옵션 타입

Optional<T> 객체는 T 타입 객체가 있거나 없을 때의 래퍼다. T 타입 객체가 있는 경우 값이 있다고 말한다.

Optional<T>는 객체나 null을 가리키는 T 타입의 참조보다 안전하다. 올바르게 사용할 경우에 한정해서.



2.1 옵션 값을 사용하는 방법

Optional을 효과적으로 사용하려면 값이 없을 때는 대체 값을 생산하고, 값이 있을 때만 해당 값을 소비하는 메서드를 사용해야 한다.

orElse : 일치하는 값이 없으면 기본값들 돌려준다.

String[] strArr = {  "fred", "john", "mike", "lee", "bong", "kim", "kate", "sanchez", "gerrard", "humbahumba" };
List<String> words = Arrays.asList(strArr);

Optional<String> optionalValue = words.stream().filter(s -> s.startsWith("l")).findFirst();
String result = optionalValue.orElse("");
// lee

orElseGet : 기본값을 계산하는 코드를 호출할 수 있다.

result = optionalValue.orElseGet(() -> System.getProperty("user.dir"));
System.out.println(result);
// /Users/seungui/Desktop/Study/java8
// 필요할 때만 메서드가 호출된다.

orElseThrow : 값이 없을 때 예외를 던진다.

Optional<String> optionalValue = words.stream().filter(s -> s.startsWith("x")).findFirst();
String result3 = optionalValue.orElseThrow(IllegalStateException::new);
System.out.println(result3);
/*
Exception in thread "main" java.lang.IllegalStateException
	at java.util.Optional.orElseThrow(Optional.java:290)
	at me.seungui.ch08.OptinalType.main(OptinalType.java:23)
*/

위의 메서드는 값이 없을 때 대체 값을 생성하는 방법이었고 이번에는 값이 있을 때만의 경우를 살펴본다.

ifPresent : 함수를 받고 옵션 값이 있을 때만 값이 함수로 전달 된다. 값이 없으면 아무일도 일어나지 않는다.

optionalValue.ifPresent(s -> System.out.printf(s));

// 값이 있을 때 리스트에 추가 하는 예
optionalValue.ifPresent(v -> resutls.add);
// 간략히
optionalValue.ifPresent(results::add);

ifPresent를 호출하면 함수에서 어떤 값도 반환받을 수 없다. 함수의 결과를 처리하려면 map을 사용하면 된다.

Optional<String> optionalValue = words.stream().filter(s -> s.startsWith("s")).findFirst();
Set<String> results = new HashSet<>();
Optional<Boolean> added = optionalValue.map(results::add);
System.out.println(added);

added는 세 가지 값 중 하나가 된다. optionalValue가 있을 때는 Optinal로 감싼 true 또는 false가 되고, 없을 때는 빈 Optional이 된다.



2.2 옵션 값을 사용하지 않는 방법

get 메서드는 Optional 값에 래핑된 요소가 있으면 얻어오고, 그렇지 않으면 NoSuchElementException을 던진다.

isPresent 메서드는 Optional<T> 객체가 값을 담고 있는지 알려준다.

if(optionalValue.isPresent()) {
        optionalValue.get().someMethod();
}



2.3 옵션 값 생성하기

다음은 Optional 객체를 생성하는 메서드이다.

public static Optional<Double> inverse(Double x) {
        return x == 0 ? Optional.empty() : Optional.of(1 / x);
}



2.4 flatMap으로 옵션 값 함수 합성하기

Optional<T>를 돌려주는 메서드 f가 있고, 타깃 타입 T에는 Optional<U>를 돌려주는 메서드 g가 포함되어 있다고 가정한다. 만일 일반 메서드라면 s.f().g()를 호출하여 메서드를 합성할 수 있다. 하지만 이렇게 호출하면 s.f()는 T가 아닌 Optional<T> 타입이 되므로 합성되지 않는다. 이럴 때는 이렇게 호출한다.

Optional<U> result = s.f().flatMap(T::g);
// s.f()가 있으면 g가 적용된다. s.f()가 없으면 비어있는 Optional<U>가 반환된다.

Optional 값을 돌려주는 다른 메서드나 람다가 있따면 이 과정을 반복할 수 있따. 호출을 flatMap으로 연쇄하는 방법을 이용해 파이프라인을 구축할 수 이따. 이 파이프라인은 모든 부분이 성공해야 전체가 성공한다.

public static void main(String[] args) {
        System.out.println(inverse(4.0).flatMap(OptinalType::squareRoot));
        System.out.println(inverse(-1.0).flatMap(OptinalType::squareRoot));
        System.out.println(inverse(0.0).flatMap(OptinalType::squareRoot));
        Optional<Double> result = Optional.of(-4.0).flatMap(OptinalType::inverse)
                .flatMap(OptinalType::squareRoot);
        System.out.println(result);
        /*
        Optional[0.5]
        Optional.empty
        Optional.empty
        Optional.empty
        */
}

public static Optional<Double> inverse(Double x) {
        return x == 0 ? Optional.empty() : Optional.of(1 / x);
}

public static Optional<Double> squareRoot(Double x) {
        return x < 0 ? Optional.empty() : Optional.of(Math.sqrt(x));
}