5.1 필터링
5.1.1 프레디케이트로 필터링
스트림 인터페이스는 filter 메서드를 지원한다. filter 메서드는 프레디케이트를 인수로 받아서 프레디케이트와 일치하는 모든 요소를 포함하는 스트림을 반환한다.
/**
* 5.1.1 프레디케이트로 필터링
*/
List<Dish> menu = new ArrayList<>();
List<Dish> vegetarianMenu = menu.stream()
.filter(Dish::isVegetarian) // 채식 요리인지 확인하는 메서드참조
.collect(Collectors.toList());
5.2 스트림 슬라이싱
5.2.1 프레디케이트를 이용한 슬라이싱
TAKEWHILE 활용
List<Dish> specialMenu = Arrays.asList(
new Dish("seasonal fruit", true, 120, Type.OTHER),
new Dish("prawns", false, 300, Type.FISH),
new Dish("rice", true, 350, Type.OTHER),
new Dish("chicken", false, 400, Type.MEAT),
new Dish("french fries", true, 530, Type.OTHER)
);
List<Dish> filteredMenu = specialMenu.stream()
.filter(dish -> dish.getCalories() < 320)
.collect(Collectors.toList());
위 리스트는 이미 칼로리 순으로 정렬되어 있다. 리스트가 이미 정렬되어 있다는 사실을 이용해서 320칼로리보다 크거나 같은 요리가 나왔을때 반복 작업을 중단 할 수 있다. 작은 리스트에서는 별거 아닌 것처럼 보일 수 있지만, 아주 많은 요소를 포함하는 큰 스트림에서는 상당한 차이가 될 수 있다.
takeWhile을 이용하면 무한스트림을 포함한 모든 스트림에 프레디케이트를 적용해 스트림을 슬라이스 할 수 있다.
// takeWhile 적용
List<Dish> slicedMenu1 = specialMenu.stream()
.takeWhile(dish -> dish.getCalories() < 320)
.collect(Collectors.toList());
DROPWHILE 활용
320 칼로리보다 큰 요소를 찾는방법으로는 dropWhile을 활용
// dropWhile 적용
List<Dish> slicedMenu2 = specialMenu.stream()
.dropWhile(dish -> dish.getCalories() < 320)
.collect(Collectors.toList());
dropWhile은 takeWhile과 정반대의 작업을 수행한다. dropWhile은 프레디케이트가 처음으로 거짓이 되는 지점까지 발견된 요소를 버린다.프레디케이트가 거짓이 되면 그 지점에서 작업을 중단하고 남은 모든 요소를 반환한다. dropWhile은 무한한 남은 요소를 가진 무한 스트림에서도 동작한다.
5.2.2 스트림 축소
스틀미은 주어진 값 이하의 크기를 갖는 새로운 스트림을 반환하는 limit(n) 메서드를 지원한다. 스트림이 정렬되어 있으면 최대 요소 n개를 반환 할 수 있다.
5.2.3 요소 건너뛰기
스트림은 처음 n개 요소를 제외한 스트림을 반환하는 skip(n) 메서드를 지원한다. n개 이하의 요소를 포함하는 스트림에 skip(n)을 호출하면 빈스트림이 반환된다.
/**
* 5.2.3 요소 건너뛰기
*/
List<Dish> menu = new ArrayList<>();
List<Dish> dishes = menu.stream()
.filter(d -> d.getCalories() > 300)
.skip(2)
.collect(Collectors.toList());
5.3 매핑
5.3.1 스트림의 가그 요소에 함수 적용하기
스트림은 함수를 인수로 받는 map 메서드를 지원한다. 인수로 제공된 함수는 각요소에 적용되며 함수를 적용한 결과가 새로운 요소로 매핑된다.
/**
* 5.3.1 스트림의 각 요소에 함수 적용하기
*/
List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(Collectors.toList());
getName은 문자열을 반환하므로 map 메서드의 출력 스트림은 Stream<String> 형식을 갖는다.
요리명의 문자길이를 알고 싶다면 map메서드를 연결 할 수 있다.
// 요리명의 길이 추가
List<Integer> dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(Collectors.toList());
5.3.2 스트림 평면화
리스트에서 고유문자로 이루어진 리스트를 반환한다. 중복을 제거하기위해 Distinct를 사용하여 중복을 제거할 수 있을거라 생각하지만,
결과는 다음과같다.
/**
* 5.3.2 스트림 평면화
*/
List<String> list = Arrays.asList("Hello", "World");
List<String[]> result = list.stream()
.map(word -> word.split(""))
.distinct().collect(Collectors.toList());
result.stream().forEach(x -> System.out.println(Arrays.toString(x)));
결과 : [H, e, l, l, o], [W, o, r, l, d]
map과 Arrays.stream 활용
우선 배열 스트림 대신 문자열 스트림이 필요하다.
/**
* map과 Arrays.stream 활용
*/
List<String> words = null;
String[] arraysOfWord = {"Goodbye", "World"};
Stream<String> streamOfWords = Arrays.stream(arraysOfWord);
words.stream()
.map(word -> word.split("")) // 각 단어를 개별 문자열로 반환
.map(Arrays::stream) // 각 배열을 별도의 스트림으로 생성
.distinct()
.collect(Collectors.toList());
결국 스트림 리스트가 만들어지면서 문제가 해결되지는 않는다. 문제를 해결하려면 먼저 각 단어를 개별 문자열로 이루어진 배열로 만든 다음에 각 배열을 별도의 스트림으로 만들어야한다.
flatMap 사용
/**
* flatMap 사용
*/
List<String> uniqueCharacters = words.stream()
.map(word -> word.split("")) // 각 단어를 개별 문자를 포함하는 배열로 변환
.flatMap(Arrays::stream) // 생성된 스트림을 하나의 스트림으로 평면화
.distinct()
.collect(Collectors.toList());
map(Arrays::stream)과 달리 flatMap(Arrays::stream)은 하나의 평면화된 스트림을 반환한다.
요약하면 flatMap 메서드는 스트림의 각 값을 다른 스트림으로 만든 다음에 모든 스트림을 하나의 스트림으로 연결하는 기능을 수행한다.
5.4 검색과 매칭
5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
/**
* 5.4.1 프레디케이트가 적어도 한 요소와 일치하는지 확인
*/
if (menu.stream().anyMatch(Dish::isVegetarian)) {
System.out.println("vegetarian food");
}
anyMatch는 Boolean을 반환하므로 최종 연산이다.
5.4.2 프레디케이트가 모든욧고와 일치하는지 검사
allMatch 메서드는 anyMatch와 달리 스트림의 모든 요소가 주어진 프레디케이트와 일치하는지 검사한다.
/**
* 5.4.2 프레디케이트가 모든욧고와 일치하는지 검사
*/
boolean isHealthy1 = menu.stream()
.allMatch(dish -> dish.getCalories() < 1000);
noneMatch
noneMatch는 allMatch와 반대연산을 수행한다. 즉, 주어진 프레디케이트와 일치하는 요소가 없는지 확인한다.
/**
* noneMatch
*/
boolean isHealthy2 = menu.stream()
.noneMatch(dish -> dish.getCalories() >= 1000);
5.4.3 요소 검색
findAny 메서드는 현재 스트림에서 임의의 요소를 반환한다. findAny 메서드를 다른 스트림연산과 연결해서 사용 할 수 있다.
/**
* 5.4.3 요소 검색
*/
Optional<Dish> dish = menu.stream()
.filter(Dish::isVegetarian)
.findAny();
스트림 파이프라인은 내부적으로 단일 과정으로 실행 할 수 있도록 최적화된다. 결과를 찾는 즉시 실행을 종료한다.
5.4.4 첫번째 요소 찾기
/**
* 5.4.4 첫번째 요소 찾기
*/
List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> first = someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst();
※ 병렬 실행에서는 첫번째 요소를 찾기 어렵다. 따라서 요소의 반환 순서가 상관없다면 병렬 스트림에서는 제약이 적은 findAny를 사용한다.
5.5 리듀싱
리듀싱연산은 모든 스트림 요소를 처리해서 값으로 도출한다.
5.5.1 요소의 합
reduce는 두개의 인수를 갖는다.
/**
* 5.5.1 요소의 합
*/
List<Integer> numbers = Arrays.asList(4, 5, 3, 9);
Integer reduce = numbers.stream().reduce(0, (a, b) -> a + b);
System.out.println(reduce);
초깃값 0, 두요소를 조합해서 새로운 값을 만드는 BinaryOperator를 사용
reduce의 내부 동작 : 초깃값 0 + 4 -> 4 + 5 -> 9 + 3 -> 12 + 9 -> 21
메서드 참조를 이용한 코드의 간결화 적용
/**
* 메서드 참조 사용
*/
Integer reduce1 = numbers.stream().reduce(0, Integer::sum);
초깃값 없음
스트림에 아무 요소도 없는 상황이라면 초깃값이 없으므로 reduce는 합계를 반환 할 수 없다. 따라서 합계가 없음을 가리킬 수 있도록
Optional로 반환한다.
/**
* 초깃값 없음
*/
Optional<Integer> reduce2 = numbers.stream().reduce((a, b) -> (a + b));
5.5.2 최댓값과 최솟값
/**
* 5.5.2 최댓값과 최솟값
*/
Optional<Integer> reduceMax = numbers.stream().reduce(Integer::max);
Optional<Integer> reduceMin = numbers.stream().reduce(Integer::min);
5.7 숫자형 스트림
메뉴의 칼로리 합계 계산
Integer reduce = menu.stream()
.map(Dish::getCalories)
.reduce(0, Integer::sum);
위코드에는 박싱 비용이 숨어있다. 내부적으로 합계를 계산하기 전에 Integer를 기본형으로 언박싱해야한다.
5.7.1 기본형 특화 스트림
Java 8에서는 세가지 기본형 특화 스트림을 제공한다. 스트림 API는 박싱 비용을 피할 수 있도록 IntStream, DoubleStream, LongStream을 제공한다. 특화 스트림은 오직 박싱 과정에서 일어나는 효율성과 관련이 있으며, 스트림에 추가 기능을 제공하지는 않는다.
숫자 스트림으로 매핑
스트림을 특화 스트림으로 변환 할 때는 mapToInt, mapToDouble, mapToLong 세가지 메서드를 가장 많이 사용한다.
map과 정확히 같은 기능을 수행하지만, Stream<T> 대신 특화된 스트림을 반환한다.
// 숫자 스트림으로 매핑
int sum = menu.stream()
.mapToInt(Dish::getCalories)
.sum();
mapToInt 메서드는 각 요리에서 모든 칼로리를 추출한 다음에 IntStream을 반환한다. IntStream 인터페이스에서 제공하는 sum 메서드를 이용해서 칼로리 합계를 계산하였다.
객체 스트림으로 복원하기
// 객체 스트림으로 복원하기
IntStream intStream = menu.stream()
.mapToInt(Dish::getCalories); // Stream -> IntStream 변환
Stream<Integer> boxed = intStream.boxed(); // IntStream -> Stream 변환
기본값 : OptionalInt
Stream에서 요소가 없는 상황과, 실제 최댓값이 0인 상황을 어떻게 구별할까? Optional을 Integer, String 등의 참조형식으로 파라미터화 할 수 있다. 또한 OptionalInt, OptionalDouble, OptionalLong 세가지 기본형 특화 스트림도 제공한다.
// 기본값 : OptionalInt
OptionalInt max = menu.stream().mapToInt(Dish::getCalories).max();
5.7.2 숫자 범위
Java 8의 IntStream과 LongStream에서는 range와 rangeClosed 두 정적 메서드를 제공한다.
range 메서드는 시작값과 종료값이 결과에 포함되지 않는 반면, rangeClosed는 시작값과 종료값이 결과에 포함된다.
IntStream intStream1 = IntStream.rangeClosed(1, 100) // 1부터 100까지
.filter(x -> x % 2 == 0); // 짝수
5.8 스트림 만들기
5.8.1 값으로 스트림 만들기
임의의 수를 인수로 받은 정적 메서드 Stream.of를 이용해서 스트림을 만들 수 있다.
/**
* 5.8.1 값으로 스트림 만들기
*/
Stream<String> modern = Stream.of("Modern", "Java", "In", "Action");
modern.map(String::toUpperCase).forEach(System.out::println);
Stream<Object> empty = Stream.empty(); // empty메서드를 이용 Stream을 비울수 있다.
5.8.2 null이 될 수 있는 객체로 스트림 만들기
Java 9에서는 null이 될 수 있는 개체를 스트림으로 만들수 잇는 메서드가 추가되었다. 예를들어 System.getProperty는 제공된 키에 대응하는 속성이 없으면 null을 반환한다.
/**
* 5.8.2 null이 될 수 있는 객체로 스트림 만들기
*/
String homeValue = System.getProperty("home");
Stream<String> homeValueStream1 = homeValue == null ? Stream.empty() : Stream.of("value");
// ofNullable 사용
Stream<String> homeValueStream2 = Stream.ofNullable(System.getProperty("home"));
ofNullable의 내부구조는 다음과 같다.
5.8.3 배열로 스트림 만들기
배열을 인수로 받는 정적 메서드 Arrays.stream을 이용해서 스트림을 만들수 있다.
/**
* 5.8.3 배열로 스트림 만들기
*/
int[] numbers = {2, 3, 4, 5, 6, 7, 8};
int sum = Arrays.stream(numbers).sum();
5.8.4 파일로 스트림 만들기
/**
* 5.8.4 파일로 스트림 만들기
*/
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) {
throw new RuntimeException(e);
}
Stream은 AutoiCloseable 인터페이스를 구현한다. 따라서 try 블록 내의 자원은 자동으로 관리된다.
5.8.5 함수로 무한 스트림 만들기
iterate 메서드
// iterate 메서드
Stream.iterate(0, n -> n+2) // 초기값 0에서 +2 증가
.limit(10) // 10개
.forEach(System.out::println);
limit을 주석처리시 무한으로 값이 증가한다.
// 초깃값 0에서, 0보다 작을때가지, +4 증가
IntStream.iterate(0, n -> n < 100, n -> n + 4)
.forEach(System.out::println);
// filter 메서드가 적용되지않음, 무한 증가
IntStream.iterate(0, n -> n + 4)
.filter(n -> n < 100)
.forEach(System.out::println);
// 해결방법 : takeWhiles
IntStream.iterate(0, n -> n + 4)
.takeWhile(n -> n < 100)
.forEach(System.out::println);
generate 메서드
iterate와 비슷하게 generate도 무한 스틀미을 만들 수 있다.하지만 iterate와 달리 generate는 생산된 값을 연속적으로 계산하지 않고, 새로운 값을 생산한다.
//generate 메서드
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
마찬가지로 limit 주석시 무한 스트림이 된다.