해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.
좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!
5.3 검색과 매칭
Entity
Dish.java
package Part2.Chapter5.Chapter5_5_3.entity;
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return this.name;
}
public int getCalories() {
return this.calories;
}
public Type getType() {
return this.type;
}
public boolean isVegetarian() {
return vegetarian;
}
@Override
public String toString() {
return this.name;
}
public enum Type {
MEAT, FISH, OTHER
}
}
Main
package Part2.Chapter5.Chapter5_5_3;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import Part2.Chapter5.Chapter5_5_1.entity.Dish;
/*
* 5.3 검색과 매칭
*
* 특정 속성이 데이터 집하에 있는지 여부를 검색하는 데이터 처리도 자주 사용된다.
* 스트림 API는 allMatch, anyMatch, noneMatch, findFirst, findAny 등 다양한 유틸리티 메서드를 제공한다.
*/
public class Main_5_3 {
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));
/*
* 5.3.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
*
* 프레디케이트가 주어진 스트림에서 적어도 한 요소와 일치하는지 확인할 때
* anyMatch 메서드를 이용한다.
* 다음 코드는 menu에 채식요리가 있는지 확인하는 예제이다.
*/
if(menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}
/*
* 5.3.2 프레디케이트가 모든 요소와 일치하는지 검사
*
* allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지
* 검사한다.
* 다음 코드는 건강식(모든 요리가 1000칼로리 이하면 건강식으로 간주)인지 확인하는 코드다.
*/
if(menu.stream().allMatch(d -> d.getCalories() < 1000)) {
System.out.println("모든 식단이 건강식단 입니다!");
}
/*
* noneMatch
* noneMatch는 allMatch와 반대 연산을 수행한다.
* 즉, noneMatch는 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.
*/
if(menu.stream().noneMatch(d -> d.getCalories() >= 1000)) {
System.out.println("건강 식단이라서 먹을게 없네요.");
}
/*
* anyMatch, allMatch, noneMatch 세 가지 메서드는 쇼트서킷 기법.
* 즉, 자바의 &&, ||와 같은 연산을 활용한다.
*/
/*
* 쇼트서킷 평가
* 때로는 전체 스트림을 처리하지 않았더라도 결과를 반환할 수 있다.
* 예를 들어 여러 and 연산으로 연결된 커다란 불린 표현식을 평가한다고 가정하자.
* 표현식에서 하나라도 거짓이라는 결과가 나오면 나머지 표현식의 결과와 상관없이
* 전체 결과도 거짓이 된다.
* 이러한 상황을 "쇼트서킷"이라고 한다.
*
* allMatch, noneMatch, findFirst, findAny 등의 연산은 모든 스트림의 요소를 처리하지
* 않고도 결과를 반환할 수 있다.
* 이는 원하는 요소를 찾으면 즉시 결과를 반환할 수 있기 때문이다.
* 마찬가지로 스트림의 모든 요소를 처리할 필요 없이 주어진 크기의 스트림을 생성하는
* limit도 쇼트서킷 연산이다.
*/
/*
* 5.3.3 요소 검색
*
* findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다.
* findAny 메서드를 다른 스트림 연산과 연결해서 사용할 수 있다.
* 다음 코드는 filter와 findAny를 이용해서 채식요리를 선택한다.
*
* 스트림 파이프라인은 내부적으로 단일 과정으로 실행할 수 있도록 최적화된다.
* 즉, 쇼트서킷을 이용해서 결과를 찾는 즉시 실행을 종료한다.
*/
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
/*
* Optional이란?
* Optional<T>클래스(java.util.Optional)는 값의 존재나 부재 여부를 표현하는 컨테이너 클래스다.
* 이전 예제에서 findAny 메서드는 아무 요소도 반환하지 않을 수 있다.
* null은 쉽게 에러를 일으킬 수 있으므로 자바 8 라이브러리 설계자는 Optional<T>라는 기능을 만들었다.
* - isPresend()는 Optional이 값을 포함하면 참(true)을 반환하고, 값을 포함하지 않으면
* 거짓(false)을 반환한다.
*
* - ifPresend(Consumer<T> block)은 값이 있으면 주어진 블럭을 실행한다.
* Consumer 함수형 인터페이스에선 T형식의 인수를 받으며 void를 반환하는 람다를 전달할 수 있다.
*
* - T get()은 값이 존재하면 값을 반환하고, 값이 없으면 NoSuchElementException을 일으킨다.
*
* - T orElse(T other)는 값이 있으면 값을 반환하고, 값이 없으면 기본값을 반환한다.
*/
// 아래 코드 Optional<Dish>에서는 요리명이 null인지 검사할 필요가 없다.
dish.ifPresent(d -> System.out.println("채식 요리를 선택한 사람 : " + d.getName() ));
/*
* 5.3.4 첫 번째 요소 찾기
*
* 리스트 또는 정렬된 데이터로부터 생성된 스트림은 논리적인 아이템 순서가 정해져 있을 수 있다.
* 이런 스트림에서 첫 번째 요소를 찾으려면 어떻게 해야 할까?
* 아래 코드는 숫자 리스트에서 3으로 나누어떨어지는 첫 번째 제곱값을 구하는 예이다.
*/
System.out.println("5.3.4 첫 번째 요소 찾기 : " + Arrays.asList(1, 2, 3, 4, 5).stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst()
.get() );
/*
* findfirst와 findAny는 언제 사용하나?
* 두 가지 메서드가 필요한 이유는 바로 병렬성 때문이다.
* 병렬실행에서 첫 번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면
* 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
*/
}
}
5.4 리듀싱
Entity
Dish.java
package Part2.Chapter5.Chapter5_5_4.entity;
public class Dish {
private final String name;
private final boolean vegetarian;
private final int calories;
private Type type;
public Dish(String name, boolean vegetarian, int calories, Type type) {
this.name = name;
this.vegetarian = vegetarian;
this.calories = calories;
this.type = type;
}
public String getName() {
return this.name;
}
public int getCalories() {
return this.calories;
}
public Type getType() {
return this.type;
}
public boolean isVegetarian() {
return vegetarian;
}
@Override
public String toString() {
return this.name;
}
public enum Type {
MEAT, FISH, OTHER
}
}
Main
package Part2.Chapter5.Chapter5_5_4;
import java.util.Arrays;
import java.util.List;
/*
* 5.4 리듀싱
*
* 스트림의 모든 요소를 반복적으로 처리하는 행위에 대한 질의를 리듀싱 연산이라고 한다.
* (모든 스트림 요소를 처리해서 값으로 도출하는)
*
* 함수형 프로그래밍 언어 용어로는 이 과정이 마치 종이(우리는 스트림)를 작은 조각이
* 될 때까지 반복해서 접는 것과 비슷하다는 의미로 폴드(fold)라고 부른다.
*/
public class Main_5_4 {
static public void main(String[] args) {
List<Integer> numbers = Arrays.asList(4, 5, 3, 9);
/*
* 5.4.1 요소의 합
*
* reduce 메서드를 보기 전에 for-each를 이용해서 리스트의 숫자 요소를 더하는 코드를 보자.
* int num = 0;
* for(int x : numbers) {
* sum += x;
* }
* numbers의 각 요소는 결과에 반복적으로 더해진다. 리스트에서 하나의 숫자가 남을 때까지
* 이 과정은 반복한다. 코드에는 두 개의 파라미터가 사용되었다.
* - sum 변수의 초기값 0
* - 리스트의 모든 요소를 조합하는 연산(+)
*/
/*
* reduce는 두 개의 인수를 갖는다.
* - 초기값 0
* - 두 요소를 조합해서 새로운 값을 만드는 BinaryOprator<T>
*
* 아래 코드 (a, b) -> a + b 관련해서 reduce 메서드가 처리하는 흐름을 설명 하자면
* (처음은 reduce 메서드의 첫 번쨰 인자로 받은 값을 더하면서 시작한다.)
* 1. 0 + 4 = (0, 4) -> 0 + 4;
* 2. 4 + 5 = (4, 5) -> 4 + 5
* 3. 9 + 3 = (9, 3) -> 9 + 3;
* 4. 12 + 9 = (12, 9) -> 12 + 9;
* 흐름과 같는다. 즉,
* ("람다 표현식에서 반환된 값", "요소") -> "람다 표현식에 반환된 값" + "요소";
* 같은 형식으로 인수가 전달되게 된다.
*/
System.out.println("5.4.1 요소의 합 - 람다 표현식 : " + numbers.stream().reduce(0, (a, b) -> a + b));
System.out.println("5.4.1 요소의 합 - 메서드 레퍼런스 : " + numbers.stream().reduce(0, Integer::sum));
/*
* 초기값 없음
*
* 초기값이 없는 reduce도 있다. 그러나 이 reduce는 Optional 객체를 반환한다.
* 스트림에 아무 요소도 없는 상황을 생각해보자. 이런 상황이라면 초기값이 없으므로 reduce는
* 합계를 반환할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록 Optional 객체로 감싼 결과를 반환한다.
*/
System.out.println("초기값 없는 reduce 메서드 : " + numbers.stream().reduce((a, b) -> a + b).get());
/*
* 5.4.2 최대값과 최소값
*
* reduce 연산은 새로운 값을 이용해서 스트림의 모든 요소를 소비할 때까지 람다를 반복 수행한다.
* 아래는 최대값과 최소값을 구하는 코드들이다.
*/
// 최대값 구하기
System.out.println("5.4.2 최대값 - 람다 표현식 : " + numbers.stream().reduce((a, b) -> a < b ? b : a).get());
System.out.println("5.4.2 최대값 - 메서드 레퍼런스 : " + numbers.stream().reduce(Integer::max).get());
// 최소값 구하기
System.out.println("5.4.2 최소값 - 람다 표현식 : " + numbers.stream().reduce((a, b) -> a < b ? a : b).get());
System.out.println("5.4.2 최소값 - 메서드 레퍼런스 : " + numbers.stream().reduce(Integer::min).get());
/*
* reduce 메서드의 장점과 병렬화
*
* reduce를 이용하면 내부 반복이 추상화되면서 내부 구현에서 병렬로 reduce를 실행할 수 있게 된다.
* 반복적인 합계에서는 sum 변수를 공유해야 하므로 쉽게 병렬화하기 어렵다.
* 강제적으로 동기화시킨다 하더라도 결국 병렬화로 얻어야 할 이득이 스레드 간의 소모적인 경쟁 때문에
* 상쇄되어 버리기 때문이다.
*/
/*
* 아래 코드는 parallelStream 메서드를 이용해서 병렬처리를 수행한다. 하지만 병렬로 실행하려면 대가를 지불해야 한다.
* 즉, reduce에 넘겨준 람다의 상태(인스턴스 변수 같은)가 바뀌지 말아야 하며, 연산이 어떤 순서로 실행되더라도
* 결과가 바뀌지 않는 구조여야 한다.
*/
System.out.println("reduce 메서드의 장점과 병렬화 : " + numbers.parallelStream().reduce(0, Integer::sum));
/*
* 스트림 연산 : 상태 없음과 상태 있음
*
* map, filter 등은 입력 스트림에서 각 요소를 받아 0 또는 결과를 출력 스트림으로 보낸다.
* 따라서(사용자가 제공한 람다나 메서드 레퍼런스가 내부적인 가변 상태를 갖지 않는다는 가정 하에)
* 이들은 보통 상태가 없는, 즉 내부 상태를 갖지 않는 연산(stateless operation)이다.
*
* 하지만 sorted나 distinct는 filter나 map과는 다르다.
* 스트림의 요소를 정렬하거나 중복을 제거하려면 과거의 이력을 알고 있어야 한다.
* 즉, 어떤 요소를 출력 스트림으로 추가하려면 "모든 요소가 버퍼에 추가되어 있어야 한다."
* 연산을 수행하는데 필요한 저장소 크기는 정해져있지 않기 때문에 데이터 스트림의 크기가 크거나
* 무한이라면 문제가 생길수 있다. 따라서 이러한 연산을 내부 상태를 갖는 연산(stateful operation)으로
* 간주할 수 있다.
*/
/*
* 중간 연산과 최종 연산
*
* 연산 : filter
* 형식 : 중간 연산
* 반환 형식 : Stream<T>
* 사용된 함수형 인터페이스 형식 : Predicate<T>
* 함수 디스크립터 : T -> boolean
*
* 연산 : distinct
* 형식 : 중간 연산(상태 있는 언바운드)
* 반환 형식 : Stream<T>
* 사용된 함수형 인터페이스 형식 :
* 함수 디스크립터 :
*
* 연산 : 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
*
* 연산 : noneMath
* 형식 : 최종 연산
* 반환 형식 : 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가 관리하므로 편리하게 데이터 관련 작업을 할 수 있다.
- 스트림 API 내부적으로 다양한 최적화가 이루어질 수 있다.
- 내부 반복뿐 아니라 코드를 병렬로 실행할지 여부도 결정할 수 있다. 이러한 일은 순차적인 반복을 단일 스레드로 구현했기 때문에 외부 반복으로는 불가능하다.
요약
- 스트림 API를 이용하면 복잡한 데이터 처리 질의를 표현할 수 있다.
- filter, distinct, skip, limit 메서드로 스트림을 필터링하거나 자를 수 있다.
- map, flatMap 메서드로 스트림의 요소를 추출하거나 변환할 수 있다.
- findFirst, findAny 메서드로 스트림의 요소를 검색할 수 있다. allMatch, noneMatch, anyMatch 메서드를 이용해서 주어진 프레디케이트와 일치하는 요소를 스트림에서 검색할 수 있다.
- 이들 메서드는 쇼트서킷(short-circuit), 즉 결과를 찾는 즉시 반환하며, 전체 스트림을 처리하지는 않는다.
- reduce 메서드로 스트림의 모든 요소를 반복 조합하며 값을 도출할 수 있다. 예를 들어 reduce로 스트림의 최대값이나 모든 요소의 합계를 계산할 수 있다.
- filter, map 등은 상태를 저장하지 않는 "상태 없는 연산(stateless operation)"이다 reduce 같은 연산은 값을 계산하는 데 필요한 상태를 저장한다. sorted, distinct 등의 메서드는 새로운 스트림을 반환하기 앞서 스트림의 모든 요소를 버퍼에 저장해야 한다. 이런 메서드를 "상태 있는 연산(stateful operation)"이라고 부른다.
- IntStream, DoubleStream, LongStream은 기본형 특화 스트림이다. 이들 연산은 각각의 기본형에 맞게 특화되어 있다.
- 컬렉션뿐 아니라, 값, 배열, 파일, iterate와 generate 같은 메서드로도 스트림을 만들 수 있다.
- 크기가 정해지지 않는 스트림을 무한 스트림이라고 한다.