해당 내용은 Java8 In Action 책을 요약 및 정리한 내용입니다.
좀 더 자세히 알고 싶으신 분들은 책을 사서 읽어보심을 추천드립니다.!
5.1 필터링과 슬라이싱
Entity
Dish.java
package Part2.Chapter5.Chapter5_5_1.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_1;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import Part2.Chapter5.Chapter5_5_1.entity.Dish;
/*
* 5.1 필터링과 슬라이싱
*/
public class Main_5_1 {
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.1.1 프레디케이트로 필터링
*
* filter 메서드는 프레디케이트(불린을 반환하는 함수)를 인수로 받아서 프레디케이트와 일치하는 모든 요소를
* 포함하는 스트림을 반환한다.
*/
menu.stream()
.filter(Dish::isVegetarian)
.collect(Collectors.toList())
.forEach(d -> System.out.println("5.1.1 프레디케이트로 필터링 : 이름 = " + d.getName() + ", 채식주의 = " + d.isVegetarian() ));
/*
* 5.1.2 고유 요소 필터링
*
* 스트림은 고유 요소로 이루어진 스트림을 반환하는 distinct라는 메서드도 지원한다.
* (고유 여부는 스트림에서 만든 객체의 hashCode, equals로 결정)
* 아래는 짝수를 선택하고 중복을 필터링하는 코드 이다.
*/
List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream()
.filter(i -> i % 2 == 0)
.distinct()
.forEach((i) -> System.out.println("5.1.2 고유 요소 필터링 : " + i));
/*
* 5.1.3 스트림 축소
*
* 스트림은 주어진 사이즈 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다.
* 아래는 300칼로리 이상의 세 요리를 선택해서 리스트를 가져오는 코드이다.
*/
menu.stream()
.filter(d -> d.getCalories() > 300)
.limit(3)
.collect(Collectors.toList())
.forEach(d -> System.out.println("5.1.3 스트림 축소 : 이름 = " + d.getName() + ", 칼로리 = " + d.getCalories() ));
/*
* 정렬되지 않는 스트림(예를 들면 소스가 Set)에도 limit를 사용할 수 있다. 소스가 정렬되어 있지 않다면
* limit의 결과도 정렬되지 않는 상태를 반환한다.
*/
/*
* 5.1.4 요소 건너뛰기
*
* 스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다.
* n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈 스트림이 반환된다.
*
* 아래 코드는 300칼로리 이상의 처음 두 요리를 건너뛴 다음에 300칼로리가 넘는 나머지 요리를 반환한다.
*/
menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(Collectors.toList())
.forEach(d -> System.out.println("5.1.4 요소 건너뛰기 : 이름 = " + d.getName() + ", 칼로리 = " + d.getCalories() ));
}
}
5.2 매핑
Entity
Dish.java
package Part2.Chapter5.Chapter5_5_2.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_2;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import Part2.Chapter5.Chapter5_5_1.entity.Dish;
/*
* 5.2 매핑
*
* 특정 객체에서 특정 데이터를 선택하는 작업은 데이터 처리 과정에서 자주 수행되는 연산이다.
* 예를 들어 SQL의 테이블에서 특정 열만 선택할 수 있다.
* 스트림 API의 map과 flatMap 메서드는 특정 데이터를 선택하는 기능을 제공한다.
*/
public class Main_5_2 {
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.2.1 스트림의 각 요소에 함수 적용하기
*
* 스트림은 함수를 인수로 받는 map 메서드를 지원한다.
* 인수로 제공된 함수는 각 요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
* (기존 값을 "고친다(modify)"라기 보단 "새로운 버전을 만든다"라는 개념에 가까우므로
* "변환(transforming)"에 가까운 "매핑(mapping)"이라는 단어를 사용한다.)
*
* getName은 문자열을 반환하므로 map 메서드의 출력 스트림은 Stream<String>형식을 갖는다.
*/
menu.stream()
.map(Dish::getName)
.collect(Collectors.toList())
.forEach(s -> System.out.println("Dish Name : " + s));
/*
* 단어 리스트가 주어졌을 때 각 단어가 포함하는 글자 수의 리스트를 반환한다 가정해보자.
* map을 이용해서 각 요소에 적용할 함수는 단어를 인수로 받아서 길이를 반환한다.
*/
Arrays.asList("Java8", "Lambdas", "In", "Action").stream()
.map(String::length)
.collect(Collectors.toList())
.forEach(len -> System.out.println("String Length : " + len));
/*
* 아래는 각 요리명의 길이를 구하는 코드이다.
*/
menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(Collectors.toList())
.forEach(len -> System.out.println("Dish Name Length : " + len));
/*
* 5.2.2 스트림 평면화
*
* 아래는 리스트에 있는 각 단어를 문자로 매핑한 다음에 distinct로 중복된 문자를 필터링 예제이다.
* 그러나 중복된 문자 필터링은 되지 않을것이다.
* 이유는 map에 전달된 람다(word -> word.split(""))가 각 단어의 String[](문자열 배열)을
* 반환하기 때문이다.
* 따라서 map의 반환 스트림도 Stream<String[]>이다. 그러나 우리가 원하는 스트림 형식은
* Stream<String>이여야 한다.
*/
Arrays.asList("Hello", "World").stream()
// map에서 Stream<String[]> 스트림을 반환한다.
.map(word -> word.split(""))
// 반환된 스트림이 Stream<String[]>이기 때문에 distinct가 되질 않는다.
.distinct()
.collect(Collectors.toList())
.forEach(word -> {
String str = "";
for(int i = 0, len = word.length; i < len; i++) {
str += word[i];
}
System.out.println(str);
});
/*
* 위 상황을 해결하기 위해서는 flatMap메서드를 사용하면 된다.
* flatMap은 각 배열을 스트림이 아니라 스트림의 콘텐츠로 매핑한다.
* 즉, map(Arrays::stream)과 달리 flatMap은 하나의 평면화된 스트림을 반환한다.
*
* flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을
* 하나의 스트림으로 연결하는 기능을 수행한다.
*/
System.out.println(Arrays.asList("Hello", "World").stream()
// map에서 Stream<String[]> 스트림을 반환한다.
.map(word -> word.split(""))
/*
* flatMap을 이용해서 하나의 평면화된 스트림을 반환한다.
* 즉 Stream<String> 스트림이 반환된다.
*/
.flatMap(Arrays::stream)
.distinct()
.collect(Collectors.toList()));
}
}
스트림 활용
- 데이터를 어떻게 처리할지는 스트림 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 같은 메서드로도 스트림을 만들 수 있다.
- 크기가 정해지지 않는 스트림을 무한 스트림이라고 한다.