이 예제 중심 자습서에서는 Java 8 스트림에 대한 심층적 인 개요를 제공합니다. StreamAPI 에 대해 처음 읽었을 때 Java I / O InputStream와 비슷하게 들리며 이름에 대해 혼란스러워했습니다 OutputStream. 하지만 Java 8 스트림은 완전히 다른 것입니다. 스트림은 모나드 이므로, 자바에 기능적 프로그래밍 을 가져 오는 데 큰 역할을합니다 .
1 2
함수 프로그래밍에서 모나드는 일련의 계단으로 정의 된 계산을 나타내는 구조입니다. 모나드 구조가있는 형식은 연산을 연결하는 의미와 해당 형식의 함수를 함께 정의합니다.
이 가이드는 Java 8 스트림을 사용하여 작업하는 방법과 다양한 종류의 사용 가능한 스트림 작업을 사용하는 방법을 설명합니다. 처리 순서 및 스트림 작업의 순서가 런타임 성능에 미치는 영향에 대해 학습합니다. 더 강력한 스트림 작업 reduce, collect그리고 flatMap자세히 설명합니다. 이 튜토리얼은 병렬 스트림에 대한 심층적 인 설명으로 끝납니다.
스트림의 작동 방식
스트림은 일련의 요소를 나타내며 이러한 요소에 대한 계산을 수행하는 다른 종류의 연산을 지원합니다.
스트림 작업은 중간 또는 터미널입니다. 중간 작업은 스트림을 반환하므로 세미콜론을 사용하지 않고 여러 중간 작업을 연결할 수 있습니다. 터미널 작업은 void이거나 비 스트림 결과를 반환합니다.
위의 예 filter에서 map, sorted은 forEach 터미널 작업의 중간 작업입니다. 사용 가능한 모든 스트림 작업의 전체 목록은 Stream Javadoc을 참조하십시오 .
위 예에서 볼 수있는 이러한 스트림 작업 체인은 작업 파이프 라인 이라고도 합니다 .
대부분의 스트림 연산은 어떤 종류의 람다 표현식 매개 변수 (operation)의 정확한 동작을 지정하는 기능적 인터페이스를 허용합니다. 이러한 작업의 대부분은 간섭이 없고 상태가 없어야합니다 .
그게 무슨 뜻 이죠?
함수는 스트림의 기본 데이터 소스를 수정하지 않을 때 간섭을받지 않습니다. 예를 들어 위 예제에서 람다 표현식은 myList컬렉션에 요소를 추가하거나 제거하여 수정하지 않습니다.
함수의 실행이 결정적이라면 함수는 상태 가 없습니다. 예를 들어 위의 예제에서 실행 중에 변경 될 수있는 외부 범위의 변경 가능한 변수 나 상태에 따라 람다식이 달라지지는 않습니다.
다른 종류의 스트림
스트림은 다양한 데이터 소스, 특히 컬렉션에서 만들 수 있습니다. 목록 및 세트 새로운 방법을 지원 stream()하고 parallelStream()중 하나에하는 것은 순차적 또는 병렬 스트림을 생성합니다. 병렬 스트림은 여러 스레드에서 작동 할 수 있으며이 자습서의 뒷부분에서 다룰 예정입니다. 현재 순차적 스트림에 중점을 둡니다.
이러한 모든 원시 스트림은 다음과 같은 차이점이있는 일반 객체 스트림과 동일하게 작동합니다. 원시 스트림은 추가 터미널 집계 연산(sum(), average())을 지원하며 특수 λ 식을 사용합니다 (예 : IntFunction대신 Function또는 IntPredicate대신 Predicate).
filter: d2 forEach: d2 filter: a2 forEach: a2 filter: b1 forEach: b1 filter: b3 forEach: b3 filter: c forEach: c
Process finished with exit code 0
이 코드 스니펫을 실행하면 콘솔에 원하는 결과가 나타납니다.
결과의 순서는 놀랄 수 있습니다. 순진한 방법은 스트림의 모든 요소에 대해 수평 적으로 연산을 차례대로 실행하는 것입니다. 그러나 대신 각 요소는 체인을 따라 세로로 이동합니다. 첫번째 문자열 “D2”는 전달 filter후 forEach 그래야만 두번째 문자열 “A2”가 처리된다.
이 동작은 다음 예제에서 볼 수 있듯이 각 요소에서 수행 된 실제 작업 수를 줄일 수 있습니다.
술어가 주어진 입력 요소에 적용되는 즉시 조작이 anyMatch리턴 true됩니다. 이것은 “A2”가 전달 된 두 번째 요소에 해당됩니다. 스트림 체인의 수직 실행 map때문에이 경우에는 두 번만 실행하면됩니다. 따라서 스트림의 모든 요소를 매핑하는 대신 map가능하면 적게 호출됩니다.
주문 문제가있는 이유
다음 예제는 두 개의 중간 작업 map및 filter터미널 작업으로 구성 forEach됩니다. 이러한 작업이 어떻게 실행되고 있는지 다시 한번 살펴 보겠습니다.
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229) at java.base/java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:538) at com.github.sejoung.codetest.stream.ReusingStreams.main(ReusingStreams.java:12)
Process finished with exit code 1
이 제한을 극복하기 위해, 우리는 실행하고자하는 모든 터미널 작업에 대해 새로운 스트림 체인을 생성해야합니다. 예를 들어, 모든 중간 작업이 이미 설정된 상태에서 새로운 스트림을 생성하기 위해 스트림 공급 업체를 생성 할 수 있습니다.
streamSupplier.get().anyMatch(s -> true); // ok streamSupplier.get().noneMatch(s -> true); // ok } }
실행결과
1
Process finished with exit code 0
각 호출 get()은 원하는 터미널 작업을 호출하기 위해 저장되는 새 스트림 을 생성합니다.
고급 작업
스트림은 다양한 작업을 지원합니다. 우리는 이미 같은 가장 중요한 작업에 대해 배운 filter나 map. 사용 가능한 다른 모든 연산을 발견하기 위해 여러분에게 맡깁니다 ( Stream Javadoc 참고 ). 대신 이제 더 복잡한 작업으로 깊은 다이빙을하자 collect, flatMap하고 reduce.
@Override public String toString(){ return name; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
package com.github.sejoung.codetest.stream;
import java.util.Arrays; import java.util.List;
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12)); } }
Collect
수집은 다른 결과의 종류, 예로 스트림의 변환 소자를 위해 매우 유용하다 단말기 동작 List, Set또는 Map. 수집 (Collect)은 공급 업체 , 누적 기 , 결합기 및 피니셔Collector 의 네 가지 작업으로 구성된 a 를 수용합니다 . 이것은 처음에는 매우 복잡해 보이지만, Java 8은 클래스 를 통해 다양한 내장 콜렉터를 지원합니다 . 따라서 가장 일반적인 작업의 경우 수집기를 직접 구현할 필요가 없습니다.Collectors
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
String phrase = persons .stream() .filter(p -> p.age >= 18) .map(p -> p.name) .collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));
System.out.println(phrase); } }
실행결과
1 2 3 4
In Germany Max and Peter and Pamela are of legal age.
Process finished with exit code 0
조인 콜렉터는 선택적 접두어 및 접미어와 구분 기호를 허용합니다.
스트림 요소를지도로 변환하려면 키와 값을 매핑하는 방법을 지정해야합니다. 매핑 된 키는 고유해야하며 그렇지 않으면 an IllegalStateException이 Throw됩니다. 선택적으로 병합 함수를 추가 매개 변수로 전달하여 예외를 무시할 수 있습니다.
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
이제 가장 강력한 빌트인 콜렉터 중 일부를 알았으니 이제는 자체 콜렉터를 만들자. 우리는 스트림의 모든 사람을 |파이프 문자 로 구분 된 상위 문자로 된 모든 이름으로 구성된 단일 문자열로 변환하려고합니다 . 이것을 달성하기 위해 우리는 새로운 컬렉터를 통해 만듭니다 Collector.of(). 우리는 수집가의 4 가지 성분 , 즉 공급자 , 축 압기 , 결합기 및 피니셔 를 통과해야합니다 .
publicclassAdvancedOperations{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
Java의 문자열은 불변이므로 StringJoiner 콜렉터가 문자열을 생성하도록 하는 헬퍼 클래스가 필요 합니다. 공급자(supplier)는 초기에 적절한 구분 기호로 이러한 StringJoiner를 생성합니다. 누적기(accumulator)는 StringJoiner에 각 사람 대문자 이름을 추가하는 데 사용됩니다. 결합 자 (combiner)는 두 개의 StringJoiners를 하나로 병합하는 방법을 알고 있습니다. 마지막 단계에서 finisher는 StringJoiner에서 원하는 String을 생성합니다.
FlatMap
우리는 이미 map작업 을 활용하여 스트림의 객체를 다른 유형의 객체로 변환하는 방법을 배웠습니다 . 모든 객체는 정확히 하나의 다른 객체에만 매핑 될 수 있기 때문에지도가 다소 제한적입니다. 그러나 하나의 객체를 여러 객체로 변환하거나 전혀 변환하지 않으려면 어떻게해야할까요? 이것은 flatMap구출에 관해서입니다.
FlatMap은 스트림의 각 요소를 다른 오브젝트의 스트림으로 변환합니다. 따라서 각 객체는 스트림에 의해 지원되는 0, 하나 또는 여러 개의 다른 객체로 변환됩니다. 이러한 스트림의 내용은 반환 된 flatMap작업 스트림에 배치됩니다 .
publicclassReduceTest{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
이 reduce메서드는 BinaryOperator 누산기 함수를 허용 합니다. 이 경우 실제로 BiFunction두 피연산자가 같은 유형을 공유합니다 Person. BiFunction은 비슷 Function하지만 두 가지 인수를 허용합니다. 예제 함수는 두 사람의 나이를 비교하여 최대 연령의 사람을 반환합니다.
두 번째 reduce방법은 신원 값과 BinaryOperator누적 기를 모두 허용합니다. 이 메소드는 스트림의 다른 모든 사람들로부터 집합 된 이름과 나이를 가진 새로운 Person을 생성하는 데 사용할 수 있습니다.
publicclassReduceTest{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
publicclassReduceTest{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
publicclassReduceTest{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));
이 스트림을 병렬로 실행하면 완전히 다른 실행 동작이 발생합니다. 이제 결합자는 실제로 호출됩니다. 누산기가 병렬로 호출되기 때문에, 결합기는 별도의 누적 값을 합산하는 데 필요합니다.
다음 장에서 병렬 스트림에 대해 자세히 살펴 봅시다.
병렬 스트림
스트림을 병렬로 실행하여 많은 양의 입력 요소에서 런타임 성능을 향상시킬 수 있습니다. 병렬 스트림 ForkJoinPool은 정적 ForkJoinPool.commonPool()메서드 를 통해 사용할 수 있는 공용 객체를 사용합니다 . 기본 스레드 풀의 크기는 사용 가능한 실제 CPU 코어의 양에 따라 최대 5 개의 스레드를 사용합니다.
그 보인다 sort메인 스레드에서만 순차적으로 실행됩니다. 사실, sort병렬 스트림에서는 새로운 Java 8 메소드를 사용합니다 Arrays.parallelSort(). Javadoc 로 설명 했듯이이 방법은 정렬이 순차적으로 또는 병렬로 수행되는 경우 배열의 길이를 결정합니다.
1 2
지정된 배열의 길이가 최소 단위보다 작은 경우, 적절한 Arrays.sort 메소드를 사용해 소트됩니다.
reduce마지막 절의 예제로 돌아갑니다 .
우리는 이미 결합 자 함수가 병렬로만 호출되지만 순차 스트림에서는 호출하지 않는다는 것을 알았습니다. 실제로 어떤 스레드가 관련되어 있는지 살펴 봅시다.
publicclassParallelStream{ publicstaticvoidmain(String[] args){ List<Person> persons = Arrays.asList( new Person("Max", 18), new Person("Peter", 23), new Person("Pamela", 23), new Person("David", 12));